丹崽的技术博客

丹崽的计算机知识博客

0%

一.子查询

我们平时在查询数据的时候,特别是报表数据的时候,或多或少都会接触到 子查询 这个东西的吧。因为有些数据,他就是需要前套一层 子查询 来先拿到部分数据,然后再结合这部分的数据接着进行查询。

二.子查询出现的位置

子查询出现的位置,如果平时没有去总结一下的话,还真的不知道可以在这么多的地方出现。具体来说呢,好像每个地方都可以出现。

2.1 SELECT位置

比如 SELECT (SELECT * FROM business_order);,当然这个语句好像并没有什么意义。

2.2 FROM位置

有个场景,比如订单表在已经支付的时候我们需要批量的去修改其他列的数据,那么我们会写:

1
2
UPDATE business_order SET status = 'DEL'
WHERE id IN (SELECT id FROM business_order WHERE xxx);

这条语句乍一看没有问题,但是 MySQL 会给你报个错误:You can't specify target table 'student' for update in FROM clause,意思大概就是无法对查询的表进行更新。 那么我们这个需求又必须要实现咋办呢,在条件中套多一层:

1
2
3
4
UPDATE business_order SET status = 'DEL'
WHERE id IN (
SELECT a.id FROM (SELECT id FROM business_order WHERE xxx) a
);

那么上面的 SELECT a.id FROM (SELECT id FROM business_order WHERE xxx) a 语句有另外一个子查询出现在 FROM 位置

2.3 WHERE位置

详见 FROM位置 里面的示例,WHERE id IN ( SELECT a.id FROM (SELECT id FROM business_order WHERE xxx) a); 就是一个出现在 WHERE 位置的 子查询

2.4 其他没有意义的地方

ORDER BY GROUP BY 都是可以出现子查询的,但是并没有什么意义,就不说了。

三.子查询的分类

单独看分类这一节并没有什么意义,但因为在下面讨论查询方式的时候,需要用到这些名字。所以大概记住一个名字代表什么意思然后带着这个名字去看下面就好了。

划分维度有几个,不过我直接挑出来常用的就好了。 其实就是一个问题:跟外查询有没有关系?

3.1 有关系:相关子查询

为了有个例子,我憋着气也要写一个可以执行的带有子查询的语句:

1
2
3
4
5
6
7
SELECT * FROM 
student stu
WHERE id IN (
SELECT stu_id
FROM scope scp
WHERE scp.stu_id = stu.id
);

(我怎么会写这么无聊的代码…….. 可以看到吧,子查询 中的 scope 条件中出现了依赖外部表 student 的条件,这种查询方法就是 相关子查询

3.2 没有关系:非相关子查询

这个简单,就是子查询直接拿到外部,还可以继续执行的语句,因为并不需要依赖外部表的值。

四.子查询执行过程

上面是关联的方式,那接下来需要根据不同的查询方式进行分类。

4.1 标量子查询

举个例子:

1
2
3
4
5
SELECT * FROM 
student stu
WHERE id = (
SELECT stu_id FROM scope
)

就是外部表的一个列等于(或者其他 bool表达式> < )另外一个子查询的时候,就是 标量子查询

4.2 行子查询

1
2
3
4
5
SELECT * FROM 
student stu
WHERE (id, name) = (
SELECT stu_id, stu_name FROM scope
)

4.3 标量子查询和行子查询的两种不同子查询

4.3.1 不相关子查询

那标量子查询、行子查询是怎么进行不相关子查询的,其实不是很难: 就当成两个简单的查询就好了,先执行 SELECT stu_id FROM scope 然后再执行 SELECT * FROM student stu WHERE id = 上面结果集

4.3.2 相关子查询

相关子查询就比较麻烦了,需要两个表每个记录去循环,看是否同时满足外部查询和内部查询的条件,如果满足才加入结果集。 比如这个:

1
2
3
4
5
6
7
SELECT * FROM 
student stu
WHERE id IN (
SELECT stu_id
FROM scope scp
WHERE scp.stu_id = stu.id AND scp.scope > 90
);

会先从 student 拿出 id=1 的记录,加入成绩表的查询,看看成绩是否大于90分,如果大于90分加入结果集返回,然后再拿出 id=2 的记录。

4.4 IN子查询

4.4.1 临时表查询

IN 查询和上面两种方式不一样,因为为了防止一些问题,比如 IN ...some sql 中,后面 SQL 语句查询出来的结果集很大,可能会导致内存不足,也会因为结果集过于庞大,外部表在查询的时候无法有效的使用到索引。 所以,IN子查询 会通过一些方式,将语句转换成 内连接 的形式来进行查询。 怎么转,通过 物化表 来做,例子说:

1
2
3
4
5
6
7
SELECT *
FROM student stu
WHERE id = (
SELECT stu_id
FROM student_parent stu_par
WHERE stu_par.name LIKE '李%'
)

首先执行子查询,即查询学生的父母姓李的所有 stu_id 数据。 那么假设,这个系统刚运行没多久,那么这个结果集(只有一列因为我们指定了查询 stu_id,并且已经去重,比如某学生的父母都姓李,那么这张临时表只会保存一个 stu_id)将会被变为一张临时表(物化成一张表)存储在内存中,并且这个表是基于 HASH索引 而存在的,然后,将 student 表与内存中的这张表进行连接,比如内存中这张物化表的名字是 student_parent_memory 那么上面这条内连接的 SQL 将进一步变成:

1
2
3
SELECT stu.* 
FROM student stu
INNER JOIN student_parent_memory spm ON stu.id = spm.*;

再假设,系统运行的不错,一直在运行着,那么子查询的表查询出来的结果集已经超过了数据库系统设置的 tmp_table_sizemax_heap_table_size。那么这个临时表存储的位置将发生了变化,表来是内存的基于 HASH索引 的表,现在就变成了硬盘中基于 B+树 的表而存在。 至于后续如何进行,就是上一节中说的连接基于成本来判断使用哪张表做 驱动表 以及 被驱动表 的策略了。

4.4.2 半连接

半连接可以说,为了不像上面一样创建临时表或者优化创建的临时表,然后去进行查询的一种策略。 仅适用于不相关子查询 方式一 Table Pullout 把表拉出去 如果关联查询的子查询的结果集的那个列是唯一索引,那么子查询将被跳过生成临时表的步骤,然后被拉到外面直接内连查询! 方式二 DuplicateWeedout execution strategy 消除重复值 生成一个只有一个列并且这个列 PRIMARY KEY,然后根据外部表的列来排除已经被加入到结果集的数据(因为这个列是唯一的)这种方式可以用于统计省市情况,由于满足某个条件(比如人口大于某个比率),而省可能包含很多个市,可能这个省多个市可以加入结果集,那么就可以使用这种方式来消除重复的省结果集。 方式三 LooseScan execution strategy 松散策略

1
2
3
4
5
6
SELECT * 
FROM order_info oi
WHERE order_id IN (
SELECT order_id FROM deliver_bill db WHERE db.order_id LIKE 'XXX'
)
# deliver_bill 中 order_id 是二级索引

那么在执行子查询查发货单表的时候,由于 order_id 模糊查询,而且在发货单中可能存在很多重复的记录,那么在使用发货单二级索引的时候,就可以直接跳过这些重复的订单号值,拿到不重复的值就可以了。 方式四 Semi-join Materialization execution strategy 这是上面临时表策略 方式五 FirstMatch execution strategy 匹配字表跳出字表查询 从外表取出记录去一次一次匹配字表的记录,是一种最原始的方式。如果这条记录已经满足了条件,那么假如结果集,不再去匹配子表而是回到外表,继续下一条记录。

一.子查询

我们平时在查询数据的时候,特别是报表数据的时候,或多或少都会接触到 子查询 这个东西的吧。因为有些数据,他就是需要前套一层 子查询 来先拿到部分数据,然后再结合这部分的数据接着进行查询。

二.子查询出现的位置

子查询出现的位置,如果平时没有去总结一下的话,还真的不知道可以在这么多的地方出现。具体来说呢,好像每个地方都可以出现。

2.1 SELECT位置

比如 SELECT (SELECT * FROM business_order);,当然这个语句好像并没有什么意义。

2.2 FROM位置

有个场景,比如订单表在已经支付的时候我们需要批量的去修改其他列的数据,那么我们会写:

1
2
UPDATE business_order SET status = 'DEL'
WHERE id IN (SELECT id FROM business_order WHERE xxx);

这条语句乍一看没有问题,但是 MySQL 会给你报个错误:You can't specify target table 'student' for update in FROM clause,意思大概就是无法对查询的表进行更新。 那么我们这个需求又必须要实现咋办呢,在条件中套多一层:

1
2
3
4
UPDATE business_order SET status = 'DEL'
WHERE id IN (
SELECT a.id FROM (SELECT id FROM business_order WHERE xxx) a
);

那么上面的 SELECT a.id FROM (SELECT id FROM business_order WHERE xxx) a 语句有另外一个子查询出现在 FROM 位置

2.3 WHERE位置

详见 FROM位置 里面的示例,WHERE id IN ( SELECT a.id FROM (SELECT id FROM business_order WHERE xxx) a); 就是一个出现在 WHERE 位置的 子查询

2.4 其他没有意义的地方

ORDER BY GROUP BY 都是可以出现子查询的,但是并没有什么意义,就不说了。

三.子查询的分类

单独看分类这一节并没有什么意义,但因为在下面讨论查询方式的时候,需要用到这些名字。所以大概记住一个名字代表什么意思然后带着这个名字去看下面就好了。

划分维度有几个,不过我直接挑出来常用的就好了。 其实就是一个问题:跟外查询有没有关系?

3.1 有关系:相关子查询

为了有个例子,我憋着气也要写一个可以执行的带有子查询的语句:

1
2
3
4
5
6
7
SELECT * FROM 
student stu
WHERE id IN (
SELECT stu_id
FROM scope scp
WHERE scp.stu_id = stu.id
);

(我怎么会写这么无聊的代码…….. 可以看到吧,子查询 中的 scope 条件中出现了依赖外部表 student 的条件,这种查询方法就是 相关子查询

3.2 没有关系:非相关子查询

这个简单,就是子查询直接拿到外部,还可以继续执行的语句,因为并不需要依赖外部表的值。

四.子查询执行过程

上面是关联的方式,那接下来需要根据不同的查询方式进行分类。

4.1 标量子查询

举个例子:

1
2
3
4
5
SELECT * FROM 
student stu
WHERE id = (
SELECT stu_id FROM scope
)

就是外部表的一个列等于(或者其他 bool表达式> < )另外一个子查询的时候,就是 标量子查询

4.2 行子查询

1
2
3
4
5
SELECT * FROM 
student stu
WHERE (id, name) = (
SELECT stu_id, stu_name FROM scope
)

4.3 标量子查询和行子查询的两种不同子查询

4.3.1 不相关子查询

那标量子查询、行子查询是怎么进行不相关子查询的,其实不是很难: 就当成两个简单的查询就好了,先执行 SELECT stu_id FROM scope 然后再执行 SELECT * FROM student stu WHERE id = 上面结果集

4.3.2 相关子查询

相关子查询就比较麻烦了,需要两个表每个记录去循环,看是否同时满足外部查询和内部查询的条件,如果满足才加入结果集。 比如这个:

1
2
3
4
5
6
7
SELECT * FROM 
student stu
WHERE id IN (
SELECT stu_id
FROM scope scp
WHERE scp.stu_id = stu.id AND scp.scope > 90
);

会先从 student 拿出 id=1 的记录,加入成绩表的查询,看看成绩是否大于90分,如果大于90分加入结果集返回,然后再拿出 id=2 的记录。

4.4 IN子查询

4.4.1 临时表查询

IN 查询和上面两种方式不一样,因为为了防止一些问题,比如 IN ...some sql 中,后面 SQL 语句查询出来的结果集很大,可能会导致内存不足,也会因为结果集过于庞大,外部表在查询的时候无法有效的使用到索引。 所以,IN子查询 会通过一些方式,将语句转换成 内连接 的形式来进行查询。 怎么转,通过 物化表 来做,例子说:

1
2
3
4
5
6
7
SELECT *
FROM student stu
WHERE id = (
SELECT stu_id
FROM student_parent stu_par
WHERE stu_par.name LIKE '李%'
)

首先执行子查询,即查询学生的父母姓李的所有 stu_id 数据。 那么假设,这个系统刚运行没多久,那么这个结果集(只有一列因为我们指定了查询 stu_id,并且已经去重,比如某学生的父母都姓李,那么这张临时表只会保存一个 stu_id)将会被变为一张临时表(物化成一张表)存储在内存中,并且这个表是基于 HASH索引 而存在的,然后,将 student 表与内存中的这张表进行连接,比如内存中这张物化表的名字是 student_parent_memory 那么上面这条内连接的 SQL 将进一步变成:

1
2
3
SELECT stu.* 
FROM student stu
INNER JOIN student_parent_memory spm ON stu.id = spm.*;

再假设,系统运行的不错,一直在运行着,那么子查询的表查询出来的结果集已经超过了数据库系统设置的 tmp_table_sizemax_heap_table_size。那么这个临时表存储的位置将发生了变化,表来是内存的基于 HASH索引 的表,现在就变成了硬盘中基于 B+树 的表而存在。 至于后续如何进行,就是上一节中说的连接基于成本来判断使用哪张表做 驱动表 以及 被驱动表 的策略了。

4.4.2 半连接

半连接可以说,为了不像上面一样创建临时表或者优化创建的临时表,然后去进行查询的一种策略。 仅适用于不相关子查询 方式一 Table Pullout 把表拉出去 如果关联查询的子查询的结果集的那个列是唯一索引,那么子查询将被跳过生成临时表的步骤,然后被拉到外面直接内连查询! 方式二 DuplicateWeedout execution strategy 消除重复值 生成一个只有一个列并且这个列 PRIMARY KEY,然后根据外部表的列来排除已经被加入到结果集的数据(因为这个列是唯一的)这种方式可以用于统计省市情况,由于满足某个条件(比如人口大于某个比率),而省可能包含很多个市,可能这个省多个市可以加入结果集,那么就可以使用这种方式来消除重复的省结果集。 方式三 LooseScan execution strategy 松散策略

1
2
3
4
5
6
SELECT * 
FROM order_info oi
WHERE order_id IN (
SELECT order_id FROM deliver_bill db WHERE db.order_id LIKE 'XXX'
)
# deliver_bill 中 order_id 是二级索引

那么在执行子查询查发货单表的时候,由于 order_id 模糊查询,而且在发货单中可能存在很多重复的记录,那么在使用发货单二级索引的时候,就可以直接跳过这些重复的订单号值,拿到不重复的值就可以了。 方式四 Semi-join Materialization execution strategy 这是上面临时表策略 方式五 FirstMatch execution strategy 匹配字表跳出字表查询 从外表取出记录去一次一次匹配字表的记录,是一种最原始的方式。如果这条记录已经满足了条件,那么假如结果集,不再去匹配子表而是回到外表,继续下一条记录。

img 图来自 semi-join子查询优化 – FirstMatch策略

一.InnoDB表数据

上面聊了这么多这个结构,那个结构的。现在是不是有点好奇,InnoDB 是把数据存在哪里的。答案也很简单,存在一颗 B+ 树种。

InnoDB 中,数据所在的位置没有其他地方,就只有一颗 B+ 树。而我们自己建立的索引,也是一颗 B+ 树。存储完整数据的 B+ 树也叫 聚簇索引,而我们自定义的列索引的 B+ 树,则称为 非聚簇索引

OK,大概明白了这两个东西以后,我们一个一个拆开来说说。

二.B+树是什么

关乎数据结构的东西了,如果觉得很简单,或者说已经学过脑袋有印象,可以跳过这段一个非计算机老男人写的废话。

B+ 树,一切要从一个最简单的例子开始,就是二分查找法。

2.1 二分查找法

我们在聚合的时候,估计都玩过一个游戏,叫做大冒险。确定谁来大冒险,有个方式就是猜数字:由上一轮中的倒霉鬼,来确定一个数字,然后围成一圈的小伙伴们来猜。猜中了,就是下一个倒霉鬼。

那怎么猜的呢,这个倒霉鬼在手机上输入一个数字,盖在地上防止修改。然后从一圈中某个人开始,说一个数字,那么那个站在台上的倒霉蛋就会缩小范围,让下一个人继续猜:

比如数字是69。

那么就有下面的对话:

1
2
3
4
5
6
A: 50
倒霉:50 - 100
B:75
倒霉:50 - 75
C:69
倒霉:恭喜你答对了balabala

这个过程咧,有没有很熟悉,对,就是 二分查找法


那为啥使用二分查找法呢,肯定是因为性能好啊。

那比如我有个 有序 数组:[18, 53, 55, 147, 151, 495, 551, 606, 638, 728]

我一次查询每一个数字,平均次数是 (1+2+3+4+5+6+7+8+9+10)/10 = 5.5次,而使用二分查找法则是 4+3+2+4+3+1+4+3+2+3)/10 = 2.9次。顺序查找最坏的情况也要 10 次,而二分查找最坏是 4 次。这么好的查找效率没有理由不用他。

2.2 二叉查找树

那二分查找和树什么关系,这里就要说到 二叉查找树,根据百度百科的定义:

一棵空树,或者是具有下列性质的二叉树

(1)若左子树不空,则左子树上所有结点的值均小于它的根结点的值;

(2)若右子树不空,则右子树上所有结点的值均大于它的根结点的值;

(3)左、右子树也分别为二叉排序树;

(4)没有键值相等的结点。

那么我现在把上面的数组转换成 二叉查找树,并且给予一个查找的动图:

一.InnoDB表数据

上面聊了这么多这个结构,那个结构的。现在是不是有点好奇,InnoDB 是把数据存在哪里的。答案也很简单,存在一颗 B+ 树种。

InnoDB 中,数据所在的位置没有其他地方,就只有一颗 B+ 树。而我们自己建立的索引,也是一颗 B+ 树。存储完整数据的 B+ 树也叫 聚簇索引,而我们自定义的列索引的 B+ 树,则称为 非聚簇索引

OK,大概明白了这两个东西以后,我们一个一个拆开来说说。

二.B+树是什么

关乎数据结构的东西了,如果觉得很简单,或者说已经学过脑袋有印象,可以跳过这段一个非计算机老男人写的废话。

B+ 树,一切要从一个最简单的例子开始,就是二分查找法。

2.1 二分查找法

我们在聚合的时候,估计都玩过一个游戏,叫做大冒险。确定谁来大冒险,有个方式就是猜数字:由上一轮中的倒霉鬼,来确定一个数字,然后围成一圈的小伙伴们来猜。猜中了,就是下一个倒霉鬼。

那怎么猜的呢,这个倒霉鬼在手机上输入一个数字,盖在地上防止修改。然后从一圈中某个人开始,说一个数字,那么那个站在台上的倒霉蛋就会缩小范围,让下一个人继续猜:

比如数字是69。

那么就有下面的对话:

1
2
3
4
5
6
A: 50
倒霉:50 - 100
B:75
倒霉:50 - 75
C:69
倒霉:恭喜你答对了balabala

这个过程咧,有没有很熟悉,对,就是 二分查找法


那为啥使用二分查找法呢,肯定是因为性能好啊。

那比如我有个 有序 数组:[18, 53, 55, 147, 151, 495, 551, 606, 638, 728]

我一次查询每一个数字,平均次数是 (1+2+3+4+5+6+7+8+9+10)/10 = 5.5次,而使用二分查找法则是 4+3+2+4+3+1+4+3+2+3)/10 = 2.9次。顺序查找最坏的情况也要 10 次,而二分查找最坏是 4 次。这么好的查找效率没有理由不用他。

2.2 二叉查找树

那二分查找和树什么关系,这里就要说到 二叉查找树,根据百度百科的定义:

一棵空树,或者是具有下列性质的二叉树

(1)若左子树不空,则左子树上所有结点的值均小于它的根结点的值;

(2)若右子树不空,则右子树上所有结点的值均大于它的根结点的值;

(3)左、右子树也分别为二叉排序树;

(4)没有键值相等的结点。

那么我现在把上面的数组转换成 二叉查找树,并且给予一个查找的动图:

查询看起来是很方便,但是对数据的增删查改就不是这样的了。有时候,数据插入多了,如果不调整可能会所有都放在了左边或者右边,这样性能就会越来越接近顺序查找。那么,数学家就提出了 平衡二叉树最优二叉树 的定义。

2.3 平衡二叉树

平衡二叉树:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1。这样子就可以让树变得矮胖矮胖的好身材,至于为啥矮胖矮胖才是好身材,因为这样子的话,查询效率才高啊。

比如下面这颗,查找大于 495 的时候,简直就是线性查找的效率了。

image-20200113155115465

那长得丑怎么办,整容(旋转跳跃我闭着眼)啊,不过需要钱(性能)。

那我现在就按照 二叉查找树 那一颗,来插入一个 999,平衡二叉树是在增删改的时候通过旋转来平衡树。

那么有长得帅的读者就要问了,这个跟 B+树 是什么关系。

2.4 B+树

因为 B+树 跟上面的思想差不多,所以,现在就来说说 B+树

B+树 她不是二叉树,但是查询跟 二叉树 差不多,我先放一颗 B+树

这是一颗高度为 2 扇出为 5B+树

那这个怎么实现查找的呢,现在比方说我要查找 606 这条数据:

那么 B+树 是怎么实现新增,然后平衡的呢,跟上面一样发生旋转。旋转的规则如下:

叶子节点是第二层的数据,索引节点是第一层查找时用到的数据。那旋转的情况就是节点页有没有满了:

叶子节点 索引节点 操作
× × 直接插入
× 1. 拆分叶子节点;
2. 中间节点值存入索引节点;
3. 小于中间节点的数据放左叶子,大于或等于放入右叶子。
1. 按照上面情况拆分叶子节点;
2. 然后根据相同的操作再拆分索引节点

第一种就不用演示了,比如新增 1000 直接存入最后边的叶子节点就好了。

接下来我演示第二种,那我就插入 645 这个数字吧。

可以从图中看到,拆分页是多么耗性能的一个东西了,所以我们经常说顺序插入是最好的性能,也要求主键一般呈递增的状态。

但是当左右兄弟节点可能没有满的时候,InnoDB不会急着去拆分数据页,而是会通过旋转的方式来让树达到平衡。这里就不说了。

那如果是第三种情况呢,拆分索引页跟拆分数据页是同样的道理,我就不画出来了,只要知道如果是第三种,性能还要比第二种低一倍,因为不仅拆分数据页,还拆分索引页了。

那如果页中有记录被删除呢,怎么去平衡,这时候就有一个东西叫做 计算因子,如果删除后的页的记录数量小于 计算因子*总页数 的时候,B+ 树会去做 合并操作。那我就继续用这一组数据来做示例。

删除 606 638 以后,B+ 树就会变成下面这个样子:

所以说,B+ 树在发生了修改以后,为了保证查询效率,会对某个一部分的节点进行整容。

三.InnoDB是怎么用B+树

好,结合上一篇的 InnoDB表结构 中的页和数据行的结构,以及刚刚所说的 B+树,现在我们就可以来看看一个表中的数据,是怎样被存储以及怎样被查询的。

从之前我们知道,数据是按照数据页的方式进行存放的,而数据页里面除了记录我们的用户记录行意外,还有一些额外的属性用来表示这一页的类型(所以数据页不仅仅是存放数据的页,也可能是 undo页 等等),而数据行里面也有一些额外的属性来辅助查找。

3.1 查找数据所在的页

之前已经系统的列举过 数据页数据行 的所有属性,现在我就挑在索引中需要用到的属性放在图中就可以了,放上其他的没什么用处,也占用地方。

那我现在就假设,上一节中的叶子节点中的数据(如下图),就是我们一条记录的主键 id,因为我们的记录肯定不止有主键这么简单,所以应该还有其他数据,我暂时使用一些占位的方框做代替。

那么这时候上面在数据在 MySQL 数据页中的结构,应该是这样子的:

叶子节点之间的关系,记录类型中:2 表示最小记录,3 表示最大记录,0 表示用户记录。那如果这棵树没有非叶子节点的话,搜索就是线性的需要一个一个遍历,这时候怎么办咧,B+树 的结构就出现了。

这个结构,就是上面 B+树InnoDB 中数据页的结构了。

3.2 查找真实数据

那么,上面我演示的一个页有三个记录,但其实不止三个记录,我就假设一个页有 16 条记录(其实不止,根据数据的大小)吧。那通过索引只能找到这一页呀,难道要一个一个遍历查询相对应的数据,这时候使用遍历还是有点早了(毕竟遍历并没有那么高效),所以在每个数据页中,又使用了一个槽进行分区。

那现在把镜头切换到一个页中来:

InnoDB 会把一个页中的所有数据根据一个很小的基数,比如 4 个记录为一组,去划分分组。那么每个组在数据页中都有对应的槽来记录分组的最大值。

那按照上面的数据,比如我要查询 55

槽0 = 18,槽1 = 54,槽2 = 64,槽3 = 77。

怎么找呢,类似于上面那个二分查找的游戏,槽018,老大说 18-77 之间,槽154,老大说 54-77,直到最后,确定数字是在 槽1槽2 之间,因为 槽2 = 64 大于 55,而 槽1 = 54 小于 55。所以从 槽1 的最大值 54 开始寻找下一条记录,遍历分组里面的数据行(因为这时候需要遍历的长度已经被切到很小了,所以是时候用遍历来做了,因为用其他算法的次数跟遍历差不多),取出 55 这条数据。

那整个查询过程中,需要跟硬盘交互的并不多,我们知道,InnoDB 是通过数据页来进行硬盘与内存的沟通的,查找效率高就意味着,我需要从硬盘取数据的次数越少。

3.3 删除记录

删除记录时,InnoDB 是怎么做的,就是在之前提到的数据行的头信息的 delete_flag 设置为 1,这条记录就会被放入 垃圾链表,当又需要插入新数据的时候,会判断是否合适放在这个位置,再把原来的数据给覆盖掉。

为什么这么做无非就是减少 硬盘IO。如果真的删除的话,需要去同步硬盘,这个过程并不是很重要,所以先标记就好了。

然后这条记录的上一条记录的 next_record 字段将被重新指向当前删除记录的下一条记录。

四.聚簇索引

那聚簇索引是什么,其实看了上面的图,加点料就可以了:

其实上面的例子我使用的是 id 来做例子,但是每条记录还带有其他所有列的信息(非 NULL)的,如果有 NULL 则会被记录在记录头信息,然后节省了存储数据的空间。

而这种索引,叫做 聚簇索引,意思是什么咧,就是一条记录保存了完整的数据信息,非聚簇则不是。而每条数据都会有一个 primary key,如果没有,InnoDB 会自动加上这个隐藏的列。这也是 MySQL_InnoDB 结构的库保存数据的方式。

所以可以这么说,InnoDB 就是 B+树 构成的。而根节点从建立表开始将会被记录,之后每次查询都会从这个根节点开始查询。

五.非聚簇索引

5.1 简单非聚簇索引

非聚簇索引聚簇索引 有什么区别,区别在于 非聚簇索引 并没有存放整条记录的所有数据,只是存放了 索引列主键 而已。所以当我们通过 非聚簇索引 查询一条数据行的所有列的时候,就需要 回表 去查询其他列的信息了,也就是说需要 两次查询 才完成这个需求。

比方一个表:

1
2
3
4
5
6
CREATE TABLE trbac_user (
id BIGINT auto_increase,
name varchar(50) NOT NULL,
mobile varchar(50) NOT NULL
PRIMARY KEY(id)
)

那我使用 name 来做非聚簇索引,这时候 InnoDB 就有一颗以 name 来查询的 B+树

那我们可以看到,数据行并没有覆盖所有数据,只有 name + id。那比如我们只是查询 name 或者 id 列,就可以直接在这里返回去了。

但是如果我还想要知道 mobile 列的话,这时候就需要在上面那棵树拿到 id,然后再去 id 那棵树查询其他的列信息。

需要从下面的主键的数据页来查询其他数据。

那这里有个什么经验呢…就是我们可以根据经常使用常用的列来建立索引,先拿到 id,然后再去缓存命中,再走数据库~

单列聚簇索引还有个需要注意的点,就是这个列的辨识度要搞,如果这个列只有 MANWOMAN 的话,为这个列建立索引根本就没有什么用处,因为跟全表扫描一样的效率,优化器更偏向于全表扫描,所以切记不要给列值只有寥寥几个的列建立索引,纯属浪费。

5.2 多列非聚簇索引

如果是 name + mobile 联合索引的话,需要注意的点就有点多了:

每个数据页的排序就会变成,先按照 name 进行排序,然后再按照 mobile 进行排序。

5.4 什么时候不会使用索引

1.不满足最左匹配

我想大家都听过 最左匹配原则,就是这个意思,因为我的索引是先根据 name 排序再根据 mobile 进行排序的。那么查询的时候,如果加上 order by 的话,这个排序同样也是派上用场的。

但是如果说查询条件只是写了 WHERE mobile = '13800x1',那上面的索引就完全派不上用场。

怎么说,因为我这个索引是结合了 name 先做排序然后索引的,你只是查询 mobile 的话,就需要遍历整个索引,拿到 id 后还需要回表,这个过程跟直接扫描 聚簇索引 相比,耗费的资源更多,所以,如果一个 name + mobile 的索引,只应用于带有 name 开始的查询条件。因为 name 在前面。这就是 最左匹配原则

2.排序一个列使用升序一个使用降序

比如 WHERE name like 'xxx%' ORDER BY name DESC,mobile ASC,这样的情况只能用到 name 列的索引(也就是所有 mobile 失去了效果),然后再把所有列重新整理,依据 mobile 重新排序。

如果两个都是同一个方向的排序就不会出现这种情况。

3.VARCHAR 匹配前缀查询

WHERE name like '%狗蛋' 这种情况为什么不能使用索引呢,因为你前面未知啊,根据 name 排序的索引查询跟全表扫描有什么区别,还少了 回表 的操作,所以只能全表扫描,取出满足 xxx狗蛋 的数据了。

4.排序用到索引中没有的列

这个理解了 区别在于 非聚簇索引 并没有存放整条记录的所有数据,只是存放了 索引列主键 而已 这句话,就知道为什么了,因为索引中的排序并不适合 SQL 指定的顺序。

5.使用函数修饰值的时候

比如 WHERE LOWER(name) = 'aa',每个列都需要通过函数调用,那就跟全表扫描没什么区别了。


那如果 SQL 语句中,条件出现的顺序没有和多列索引的顺序一致的话,会怎么样:

完全不必担心这个问题,因为 SQL 在执行之前还会交给一个叫做 优化器 的东西进行整理,下面会说到,现在就只要知道他会把 SQL 条件整理成跟索引一样的顺序就好了。

既然多列索引可以帮忙查询条件中满足最左匹配的 SQL 语句,是不是越多越好,答案并不是,我觉得应该根据权重来区分,因为 非聚簇索引 他的索引列并不会像自增 ID 的索引树一样,顺序的插入数据(历史原因导致我们项目使用 UUID 做主键的哭晕在厕所),而是经常的造成 页拆分(也就是上面拆分的动图),所以 非聚簇索引 多了,增删改 需要维护这棵树平衡所做的 整容 就越多,严重的话会影响 增删改 的效率。


5.5 什么时候命中索引

那什么时候用到了索引,自然就是避免上面的情况就可以了,就有这些情况,包括但不局限于:

  1. 条件覆盖了索引的最左列;
  2. 常量查询,WHERE name = 'xxx'
  3. 范围查询,满足 1 的情况下,比如 name > 'AA' 或者 name = 'AA' AND mobile > '13800';
  4. 排序,但是需要用到的排序列是同一个方向进行排序的。
  5. 分组,依然需要满足 1 条件,如果用到了 group by nameInnoDB 会先将相同的 name 放在一起,然后再继续根据其他条件进行运算。

5.6 回表代价

可以这么说,如果回表的代价过高,InnoDB解析器 可能直接决定,不要使用索引,直接全表扫描。

怎么回事,就是 非聚簇 索引他的值都是连在一起的,查询的时候,称为 顺序I/O,而 非聚簇索引 查询出来的 id 并不会连在一起,所以回表去查询 id 数据的时候,称为 随机I/O。而 随机I/O 的效率是很低的,所以当命中索引的数据行总是较多的时候,不如 全表扫描 来的快,所以可能出现 SQL 已经完全命中这个索引但是解析器他就是不使用。

怎么防止这种情况发生,就是让查询索引返回的数量少,比如我们可以 LIMIT 100 去限定只查询 100 条数据,或者我们只要查询索引中出现的列,比如 id 然后再在程序中,根据 id 列表去命中缓存,名不中的再一次性批量查库就可以了。

5.7 文件排序

有时候我们去解析 EXPLAIN 我们的 SQL 语句的时候,会出现 File Sort 类型,这个类型指的是,查询出来的结果不是按照我们 SQL 指定的顺序来的,所以需要在内存中(必要时借助硬盘)来重新对数据进行排序,这个过程就是 File Sort,那如何避免这个类型出现,那就是防止上面不命中索引的情况。

5.8 索引合并

使用等值且命中两个索引的列

比方说我的用户表有 name 索引 和 mobile 索引,那这个条件 WHERE name = 'Weidan' AND mobile = '18588777777' 就可以使用两个索引,然后取 id交集,然后回表查询。但是如果 name > 'Weidan' 这种条件,就不会使用到合并。

但是但是,如果是主键列是范围的话,是可以的,因为他们合并的时候也会操作主键嘛。

不过有没有使用到,还要看成本,如果一个索引命中的数据太多,依然不会使用合并索引。

合并两个索引的列

依然需要满足 列都是等值查询 这个条件,比如 WHERE name = 'Weidan' OR mobile = '18588777777',会合并两个主键再统一回表查询。

范围搜索合并查询

不过如果我们是范围查询,又想使用两个索引怎么办,我们可以把 name > 'Weidan' 以及 monile > '18588777777' 先查询出来 id 然后 union 在一起,再通过 id 去寻找我们想要的数据。

但是还是建议把需要合并的列作为一个 非聚簇 索引来覆盖,效率要高。

七.表连接查询

首先就以最经典的学生、成绩来做示例吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
CREATE TABLE `student` (
`id` int(11) NOT NULL,
`name` varchar(50) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

BEGIN;
INSERT INTO `student` VALUES (1, '狗蛋');
INSERT INTO `student` VALUES (2, '狗剩');
INSERT INTO `student` VALUES (3, '翠花');
COMMIT;

DROP TABLE IF EXISTS `scope`;
CREATE TABLE `scope` (
`id` int(11) NOT NULL,
`subject` varchar(255) DEFAULT NULL,
`scope` int(255) DEFAULT NULL,
`stu_id` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

BEGIN;
INSERT INTO `scope` VALUES (1, '语文', 80, 1);
INSERT INTO `scope` VALUES (2, '数学', 98, 1);
INSERT INTO `scope` VALUES (3, '英语', 59, 2);
INSERT INTO `scope` VALUES (4, '政治', 79, 2);
COMMIT;

7.1 连表查询

那么先列举一个连表很普通的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
SELECT * 
FROM student, scope;

+----+--------+----+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+----+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 2 | 狗剩 | 1 | 语文 | 80 | 1 |
| 3 | 翠花 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 2 | 数学 | 98 | 1 |
| 3 | 翠花 | 2 | 数学 | 98 | 1 |
| 1 | 狗蛋 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 3 | 翠花 | 3 | 英语 | 59 | 2 |
| 1 | 狗蛋 | 4 | 政治 | 79 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
| 3 | 翠花 | 4 | 政治 | 79 | 2 |
+----+--------+----+---------+-------+--------+
12 rows in set (0.00 sec)

可以看到,如果我们不加任何条件,直接就连接两张表的话,取出来的结果就是第一张表所有的数据,去搭配第二张表所有的数据,也就是第一张表有 3 条数据,第二张表有 4 条记录,结果就有 3 ✖ 4 = 12 条记录。这个过程也称为 笛卡尔积。这种情况在日常编程中应该也没人会这么写的吧,仔细想想,如果两张表都是百万级别的数据,笛卡尔积是多恐怖的一件事情。

7.2 内连接

就是由于上面的结果集一般我们都不需要,所以我们会加上一些条件加以限制:

1
2
3
4
5
6
7
8
9
10
11
12
13
SELECT stu.*, scp.*
FROM student stu, scope scp
WHERE stu.id = scp.stu_id;

+----+--------+----+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+----+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
+----+--------+----+---------+-------+--------+
4 rows in set (0.00 sec)

这就是我们想要的结果集,所有参考学生的考试成绩。

InnoDB 是怎样的实现两个表连接的呢,这时候,优化器会根据查询成本确定 驱动表被驱动表

  • 驱动表:先查询的那个表,这里假设是 student 表,所以执行的时候,第一步执行 SELECT * FROM student;

  • 被驱动表:查询出来驱动表所有的数据以后,就会根据驱动表中每一条数据,去执行被驱动表。

    • 比如我现在 student 有三条记录,分别 id1 2 3
    • 然后对三条记录分别执行 SELECT * FROM scope WHERE stu_id = ?
    • 因为 scope 表中不存在 翠花 的成绩,所以 SELECT * FROM scope WHERE stu_id = 3 并没有展示出来,因为这个语句相当于 SELECT * FROM scope WHERE stu_id NOT NULL AND stu_id = 3

那如果上面的语句加上其他条件会怎么样,比如:

1
2
3
4
5
# 查询成绩 90 分以上的学生成绩信息
SELECT stu.*, scp.*
FROM student stu, scope scp
WHERE stu.id = scp.stu_id
AND scp.scope > 90;

那在访问驱动表 student 的时候同上面一样,但是针对驱动表中每一条数据执行被驱动表的时候,就会变成:

SELECT * FROM scope WHERE stu_id NOT NULL AND stu_id = ? AND scope > 90;

一句话就是说,这些条件针对哪个表,在执行那个表的时候就会带上指定的条件。

内连接推荐写法:

1
2
3
SELECT stu.*, scp.*
FROM student stu INNER JOIN scope scp ON stu.id = scp.stu_id
WHERE scp.scope > 90;

效率没有变高,但是可读性变好,在我看来,特定的限定功能就需要放在特定的位置,连接条件在 ON 后面,而查询条件在 WHERE 后面,也可以很好的跟下面的外连接写法统一。

7.3 外连接

外连接的目的是将连接在一起查询的表中,以某个表为主要目的,查询他在另外一个表的所有信息,包括不存在的信息。比如还是上面的例子,翠花她是个坏学生,一个科目都没有参加考试,但是老师如果通过上面的内连接查询来看的话,可能都不知道还有这个人的存在。这就麻烦大了啊,家长上门投诉,我给你钱了,她没参加考试你都不管管的吗。所以这时候,就需要外连接来做这个事情了:

1
2
3
4
5
6
7
8
9
10
11
12
SELECT * 
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id;
+----+--------+------+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+------+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
| 3 | 翠花 | NULL | NULL | NULL | NULL |
+----+--------+------+---------+-------+--------+
5 rows in set (0.00 sec)

这个时候,嗯哼,一抓一个准,没有来考试的,在 scope 表的字段中,显示为 NULL

关于 驱动表被驱动表 的含义已经在上面内连接的内容中指定,那么如果说 内连接 是根据成本来指定 驱动表 的,那 外连接 就是我们来指定驱动表。

驱动表 所有满足我们条件的数据,来查询 被驱动表,如果 被驱动表 不存在 驱动表 中某条数据的关联,显示为 NULL

那么关键点在哪里,就在这个 ON 后面的条件,ON 后面的条件,是指定 被驱动表 中不满足 ON 条件情况下依然要显示的关键。(在内连接中连接条件放 ON 和放 WHERE 效果一样)

如果外连接有几层,比如说三层:

1
2
3
4
SELECT *
FROM teacher t
LEFT JOIN student ON t.id = stu.into
LEFT JOIN scope scp ON stu.id = scope.stu_id;

那么上面这条查询需要怎么走呢,先查询老师所有的学生,得到一张中间表以后,再以这张中间表作为 驱动表 来查询 scope

八.复杂条件查询

MySQL InnoDB 中存在着 SQL解析器,解析器可谓为了查询正确的数据而费尽心思,目的只有一个,就是较快的查询出来正确的数据。其实这里也可以联想到 jvm编译器,为了执行效率比较快,会对字节码进行一些顺序的重新编排,什么 happen before 规则都出来了。

而什么东西查询最快呢,那就是查询常量的时候了。

8.1 整理查询语句

首先就是去除不必要的括号,因为括号如果没有意义存在,那移除了就更有利于优化器的解析了。

比如 WHERE (id = 1) 会被优化成 WHERE id = 1。这个好像也没什么好说的。

复杂一点的 WHERE (id = 1 AND name = 'WEIDAN') AND age = 20 则会被优化成 WHERE id = 1 AND name = 'WEIDAN' AND age = 20

8.2 简化查询条件

WHERE age > 18 AND real_age > age 则直接变成 WHERE age > 18 AND real_age > 18

8.3 删除废话条件

像我之前老师教的,在写 MyBatis Mapper.xml 的时候,因为 WHERE 总是需要动态的条件,所以前面会加一个 WHERE 1 = 1(为啥不用标签,因为标签可读性其实不怎么高)。

那么,如果我写的是 WHERE 1 = 1 AND name LIKE 'Weidan%',那么语句将会被直接优化为 WHERE name LIKE 'Weidan%' 。这么想想好像直接写 WHERE 1 = 1 除了解析器多了点工作,好像也没什么所谓了。

8.4 计算表达式

WHERE age = 9 + 9 那么解析器会直接给你个 WHERE age = 18。不过如果这些计算方式放在了列名上,比如:

1
2
WHERE -age = -18
WHERE to_days(age) = 18

解析器害怕他也优化错了,所以也会放弃优化,一个简单的记忆就是函数发生在等号后面的常量值上,可以优化,但是如果需要依赖列的计算的话,那就放弃优化了。

8.5 必要时转换外连接到内连接

右外连接会被转换为左外连接,毕竟 左外连接 更好确定 驱动表被驱动表

那如果我们在查询 左外连接 的时候,暗示着我得出的结果中,被驱动表不含 NULL 的值,聪明的暖男解析器将会 GET 到这个信息,把 外连接 优化成 内连接

比如上面的例子中:

1
2
3
4
5
6
7
SELECT * 
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id
AND scp.id = 1;

SELECT *
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id;
AND scp.id IS NOT NULL;

那么上面两个语句中是不是就都暗示解析器中,拿到的 scope 中的 id 不能为空。

所以!直接变成这条语句执行:

1
2
3
4
5
6
SELECT * 
FROM student stu INNER JOIN scope scp ON scp.stu_id = stu.id
AND scp.id = 1;

SELECT *
FROM student stu INNER JOIN scope scp ON scp.stu_id = stu.id;;

九.完

查询看起来是很方便,但是对数据的增删查改就不是这样的了。有时候,数据插入多了,如果不调整可能会所有都放在了左边或者右边,这样性能就会越来越接近顺序查找。那么,数学家就提出了 平衡二叉树最优二叉树 的定义。

2.3 平衡二叉树

平衡二叉树:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1。这样子就可以让树变得矮胖矮胖的好身材,至于为啥矮胖矮胖才是好身材,因为这样子的话,查询效率才高啊。

比如下面这颗,查找大于 495 的时候,简直就是线性查找的效率了。

一.InnoDB表数据

上面聊了这么多这个结构,那个结构的。现在是不是有点好奇,InnoDB 是把数据存在哪里的。答案也很简单,存在一颗 B+ 树种。

InnoDB 中,数据所在的位置没有其他地方,就只有一颗 B+ 树。而我们自己建立的索引,也是一颗 B+ 树。存储完整数据的 B+ 树也叫 聚簇索引,而我们自定义的列索引的 B+ 树,则称为 非聚簇索引

OK,大概明白了这两个东西以后,我们一个一个拆开来说说。

二.B+树是什么

关乎数据结构的东西了,如果觉得很简单,或者说已经学过脑袋有印象,可以跳过这段一个非计算机老男人写的废话。

B+ 树,一切要从一个最简单的例子开始,就是二分查找法。

2.1 二分查找法

我们在聚合的时候,估计都玩过一个游戏,叫做大冒险。确定谁来大冒险,有个方式就是猜数字:由上一轮中的倒霉鬼,来确定一个数字,然后围成一圈的小伙伴们来猜。猜中了,就是下一个倒霉鬼。

那怎么猜的呢,这个倒霉鬼在手机上输入一个数字,盖在地上防止修改。然后从一圈中某个人开始,说一个数字,那么那个站在台上的倒霉蛋就会缩小范围,让下一个人继续猜:

比如数字是69。

那么就有下面的对话:

1
2
3
4
5
6
A: 50
倒霉:50 - 100
B:75
倒霉:50 - 75
C:69
倒霉:恭喜你答对了balabala

这个过程咧,有没有很熟悉,对,就是 二分查找法


那为啥使用二分查找法呢,肯定是因为性能好啊。

那比如我有个 有序 数组:[18, 53, 55, 147, 151, 495, 551, 606, 638, 728]

我一次查询每一个数字,平均次数是 (1+2+3+4+5+6+7+8+9+10)/10 = 5.5次,而使用二分查找法则是 4+3+2+4+3+1+4+3+2+3)/10 = 2.9次。顺序查找最坏的情况也要 10 次,而二分查找最坏是 4 次。这么好的查找效率没有理由不用他。

2.2 二叉查找树

那二分查找和树什么关系,这里就要说到 二叉查找树,根据百度百科的定义:

一棵空树,或者是具有下列性质的二叉树

(1)若左子树不空,则左子树上所有结点的值均小于它的根结点的值;

(2)若右子树不空,则右子树上所有结点的值均大于它的根结点的值;

(3)左、右子树也分别为二叉排序树;

(4)没有键值相等的结点。

那么我现在把上面的数组转换成 二叉查找树,并且给予一个查找的动图:

查询看起来是很方便,但是对数据的增删查改就不是这样的了。有时候,数据插入多了,如果不调整可能会所有都放在了左边或者右边,这样性能就会越来越接近顺序查找。那么,数学家就提出了 平衡二叉树最优二叉树 的定义。

2.3 平衡二叉树

平衡二叉树:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1。这样子就可以让树变得矮胖矮胖的好身材,至于为啥矮胖矮胖才是好身材,因为这样子的话,查询效率才高啊。

比如下面这颗,查找大于 495 的时候,简直就是线性查找的效率了。

image-20200113155115465

那长得丑怎么办,整容(旋转跳跃我闭着眼)啊,不过需要钱(性能)。

那我现在就按照 二叉查找树 那一颗,来插入一个 999,平衡二叉树是在增删改的时候通过旋转来平衡树。

那么有长得帅的读者就要问了,这个跟 B+树 是什么关系。

2.4 B+树

因为 B+树 跟上面的思想差不多,所以,现在就来说说 B+树

B+树 她不是二叉树,但是查询跟 二叉树 差不多,我先放一颗 B+树

这是一颗高度为 2 扇出为 5B+树

那这个怎么实现查找的呢,现在比方说我要查找 606 这条数据:

那么 B+树 是怎么实现新增,然后平衡的呢,跟上面一样发生旋转。旋转的规则如下:

叶子节点是第二层的数据,索引节点是第一层查找时用到的数据。那旋转的情况就是节点页有没有满了:

叶子节点 索引节点 操作
× × 直接插入
× 1. 拆分叶子节点;
2. 中间节点值存入索引节点;
3. 小于中间节点的数据放左叶子,大于或等于放入右叶子。
1. 按照上面情况拆分叶子节点;
2. 然后根据相同的操作再拆分索引节点

第一种就不用演示了,比如新增 1000 直接存入最后边的叶子节点就好了。

接下来我演示第二种,那我就插入 645 这个数字吧。

可以从图中看到,拆分页是多么耗性能的一个东西了,所以我们经常说顺序插入是最好的性能,也要求主键一般呈递增的状态。

但是当左右兄弟节点可能没有满的时候,InnoDB不会急着去拆分数据页,而是会通过旋转的方式来让树达到平衡。这里就不说了。

那如果是第三种情况呢,拆分索引页跟拆分数据页是同样的道理,我就不画出来了,只要知道如果是第三种,性能还要比第二种低一倍,因为不仅拆分数据页,还拆分索引页了。

那如果页中有记录被删除呢,怎么去平衡,这时候就有一个东西叫做 计算因子,如果删除后的页的记录数量小于 计算因子*总页数 的时候,B+ 树会去做 合并操作。那我就继续用这一组数据来做示例。

删除 606 638 以后,B+ 树就会变成下面这个样子:

所以说,B+ 树在发生了修改以后,为了保证查询效率,会对某个一部分的节点进行整容。

三.InnoDB是怎么用B+树

好,结合上一篇的 InnoDB表结构 中的页和数据行的结构,以及刚刚所说的 B+树,现在我们就可以来看看一个表中的数据,是怎样被存储以及怎样被查询的。

从之前我们知道,数据是按照数据页的方式进行存放的,而数据页里面除了记录我们的用户记录行意外,还有一些额外的属性用来表示这一页的类型(所以数据页不仅仅是存放数据的页,也可能是 undo页 等等),而数据行里面也有一些额外的属性来辅助查找。

3.1 查找数据所在的页

之前已经系统的列举过 数据页数据行 的所有属性,现在我就挑在索引中需要用到的属性放在图中就可以了,放上其他的没什么用处,也占用地方。

那我现在就假设,上一节中的叶子节点中的数据(如下图),就是我们一条记录的主键 id,因为我们的记录肯定不止有主键这么简单,所以应该还有其他数据,我暂时使用一些占位的方框做代替。

那么这时候上面在数据在 MySQL 数据页中的结构,应该是这样子的:

叶子节点之间的关系,记录类型中:2 表示最小记录,3 表示最大记录,0 表示用户记录。那如果这棵树没有非叶子节点的话,搜索就是线性的需要一个一个遍历,这时候怎么办咧,B+树 的结构就出现了。

这个结构,就是上面 B+树InnoDB 中数据页的结构了。

3.2 查找真实数据

那么,上面我演示的一个页有三个记录,但其实不止三个记录,我就假设一个页有 16 条记录(其实不止,根据数据的大小)吧。那通过索引只能找到这一页呀,难道要一个一个遍历查询相对应的数据,这时候使用遍历还是有点早了(毕竟遍历并没有那么高效),所以在每个数据页中,又使用了一个槽进行分区。

那现在把镜头切换到一个页中来:

InnoDB 会把一个页中的所有数据根据一个很小的基数,比如 4 个记录为一组,去划分分组。那么每个组在数据页中都有对应的槽来记录分组的最大值。

那按照上面的数据,比如我要查询 55

槽0 = 18,槽1 = 54,槽2 = 64,槽3 = 77。

怎么找呢,类似于上面那个二分查找的游戏,槽018,老大说 18-77 之间,槽154,老大说 54-77,直到最后,确定数字是在 槽1槽2 之间,因为 槽2 = 64 大于 55,而 槽1 = 54 小于 55。所以从 槽1 的最大值 54 开始寻找下一条记录,遍历分组里面的数据行(因为这时候需要遍历的长度已经被切到很小了,所以是时候用遍历来做了,因为用其他算法的次数跟遍历差不多),取出 55 这条数据。

那整个查询过程中,需要跟硬盘交互的并不多,我们知道,InnoDB 是通过数据页来进行硬盘与内存的沟通的,查找效率高就意味着,我需要从硬盘取数据的次数越少。

3.3 删除记录

删除记录时,InnoDB 是怎么做的,就是在之前提到的数据行的头信息的 delete_flag 设置为 1,这条记录就会被放入 垃圾链表,当又需要插入新数据的时候,会判断是否合适放在这个位置,再把原来的数据给覆盖掉。

为什么这么做无非就是减少 硬盘IO。如果真的删除的话,需要去同步硬盘,这个过程并不是很重要,所以先标记就好了。

然后这条记录的上一条记录的 next_record 字段将被重新指向当前删除记录的下一条记录。

四.聚簇索引

那聚簇索引是什么,其实看了上面的图,加点料就可以了:

其实上面的例子我使用的是 id 来做例子,但是每条记录还带有其他所有列的信息(非 NULL)的,如果有 NULL 则会被记录在记录头信息,然后节省了存储数据的空间。

而这种索引,叫做 聚簇索引,意思是什么咧,就是一条记录保存了完整的数据信息,非聚簇则不是。而每条数据都会有一个 primary key,如果没有,InnoDB 会自动加上这个隐藏的列。这也是 MySQL_InnoDB 结构的库保存数据的方式。

所以可以这么说,InnoDB 就是 B+树 构成的。而根节点从建立表开始将会被记录,之后每次查询都会从这个根节点开始查询。

五.非聚簇索引

5.1 简单非聚簇索引

非聚簇索引聚簇索引 有什么区别,区别在于 非聚簇索引 并没有存放整条记录的所有数据,只是存放了 索引列主键 而已。所以当我们通过 非聚簇索引 查询一条数据行的所有列的时候,就需要 回表 去查询其他列的信息了,也就是说需要 两次查询 才完成这个需求。

比方一个表:

1
2
3
4
5
6
CREATE TABLE trbac_user (
id BIGINT auto_increase,
name varchar(50) NOT NULL,
mobile varchar(50) NOT NULL
PRIMARY KEY(id)
)

那我使用 name 来做非聚簇索引,这时候 InnoDB 就有一颗以 name 来查询的 B+树

那我们可以看到,数据行并没有覆盖所有数据,只有 name + id。那比如我们只是查询 name 或者 id 列,就可以直接在这里返回去了。

但是如果我还想要知道 mobile 列的话,这时候就需要在上面那棵树拿到 id,然后再去 id 那棵树查询其他的列信息。

需要从下面的主键的数据页来查询其他数据。

那这里有个什么经验呢…就是我们可以根据经常使用常用的列来建立索引,先拿到 id,然后再去缓存命中,再走数据库~

单列聚簇索引还有个需要注意的点,就是这个列的辨识度要搞,如果这个列只有 MANWOMAN 的话,为这个列建立索引根本就没有什么用处,因为跟全表扫描一样的效率,优化器更偏向于全表扫描,所以切记不要给列值只有寥寥几个的列建立索引,纯属浪费。

5.2 多列非聚簇索引

如果是 name + mobile 联合索引的话,需要注意的点就有点多了:

每个数据页的排序就会变成,先按照 name 进行排序,然后再按照 mobile 进行排序。

5.4 什么时候不会使用索引

1.不满足最左匹配

我想大家都听过 最左匹配原则,就是这个意思,因为我的索引是先根据 name 排序再根据 mobile 进行排序的。那么查询的时候,如果加上 order by 的话,这个排序同样也是派上用场的。

但是如果说查询条件只是写了 WHERE mobile = '13800x1',那上面的索引就完全派不上用场。

怎么说,因为我这个索引是结合了 name 先做排序然后索引的,你只是查询 mobile 的话,就需要遍历整个索引,拿到 id 后还需要回表,这个过程跟直接扫描 聚簇索引 相比,耗费的资源更多,所以,如果一个 name + mobile 的索引,只应用于带有 name 开始的查询条件。因为 name 在前面。这就是 最左匹配原则

2.排序一个列使用升序一个使用降序

比如 WHERE name like 'xxx%' ORDER BY name DESC,mobile ASC,这样的情况只能用到 name 列的索引(也就是所有 mobile 失去了效果),然后再把所有列重新整理,依据 mobile 重新排序。

如果两个都是同一个方向的排序就不会出现这种情况。

3.VARCHAR 匹配前缀查询

WHERE name like '%狗蛋' 这种情况为什么不能使用索引呢,因为你前面未知啊,根据 name 排序的索引查询跟全表扫描有什么区别,还少了 回表 的操作,所以只能全表扫描,取出满足 xxx狗蛋 的数据了。

4.排序用到索引中没有的列

这个理解了 区别在于 非聚簇索引 并没有存放整条记录的所有数据,只是存放了 索引列主键 而已 这句话,就知道为什么了,因为索引中的排序并不适合 SQL 指定的顺序。

5.使用函数修饰值的时候

比如 WHERE LOWER(name) = 'aa',每个列都需要通过函数调用,那就跟全表扫描没什么区别了。


那如果 SQL 语句中,条件出现的顺序没有和多列索引的顺序一致的话,会怎么样:

完全不必担心这个问题,因为 SQL 在执行之前还会交给一个叫做 优化器 的东西进行整理,下面会说到,现在就只要知道他会把 SQL 条件整理成跟索引一样的顺序就好了。

既然多列索引可以帮忙查询条件中满足最左匹配的 SQL 语句,是不是越多越好,答案并不是,我觉得应该根据权重来区分,因为 非聚簇索引 他的索引列并不会像自增 ID 的索引树一样,顺序的插入数据(历史原因导致我们项目使用 UUID 做主键的哭晕在厕所),而是经常的造成 页拆分(也就是上面拆分的动图),所以 非聚簇索引 多了,增删改 需要维护这棵树平衡所做的 整容 就越多,严重的话会影响 增删改 的效率。


5.5 什么时候命中索引

那什么时候用到了索引,自然就是避免上面的情况就可以了,就有这些情况,包括但不局限于:

  1. 条件覆盖了索引的最左列;
  2. 常量查询,WHERE name = 'xxx'
  3. 范围查询,满足 1 的情况下,比如 name > 'AA' 或者 name = 'AA' AND mobile > '13800';
  4. 排序,但是需要用到的排序列是同一个方向进行排序的。
  5. 分组,依然需要满足 1 条件,如果用到了 group by nameInnoDB 会先将相同的 name 放在一起,然后再继续根据其他条件进行运算。

5.6 回表代价

可以这么说,如果回表的代价过高,InnoDB解析器 可能直接决定,不要使用索引,直接全表扫描。

怎么回事,就是 非聚簇 索引他的值都是连在一起的,查询的时候,称为 顺序I/O,而 非聚簇索引 查询出来的 id 并不会连在一起,所以回表去查询 id 数据的时候,称为 随机I/O。而 随机I/O 的效率是很低的,所以当命中索引的数据行总是较多的时候,不如 全表扫描 来的快,所以可能出现 SQL 已经完全命中这个索引但是解析器他就是不使用。

怎么防止这种情况发生,就是让查询索引返回的数量少,比如我们可以 LIMIT 100 去限定只查询 100 条数据,或者我们只要查询索引中出现的列,比如 id 然后再在程序中,根据 id 列表去命中缓存,名不中的再一次性批量查库就可以了。

5.7 文件排序

有时候我们去解析 EXPLAIN 我们的 SQL 语句的时候,会出现 File Sort 类型,这个类型指的是,查询出来的结果不是按照我们 SQL 指定的顺序来的,所以需要在内存中(必要时借助硬盘)来重新对数据进行排序,这个过程就是 File Sort,那如何避免这个类型出现,那就是防止上面不命中索引的情况。

5.8 索引合并

使用等值且命中两个索引的列

比方说我的用户表有 name 索引 和 mobile 索引,那这个条件 WHERE name = 'Weidan' AND mobile = '18588777777' 就可以使用两个索引,然后取 id交集,然后回表查询。但是如果 name > 'Weidan' 这种条件,就不会使用到合并。

但是但是,如果是主键列是范围的话,是可以的,因为他们合并的时候也会操作主键嘛。

不过有没有使用到,还要看成本,如果一个索引命中的数据太多,依然不会使用合并索引。

合并两个索引的列

依然需要满足 列都是等值查询 这个条件,比如 WHERE name = 'Weidan' OR mobile = '18588777777',会合并两个主键再统一回表查询。

范围搜索合并查询

不过如果我们是范围查询,又想使用两个索引怎么办,我们可以把 name > 'Weidan' 以及 monile > '18588777777' 先查询出来 id 然后 union 在一起,再通过 id 去寻找我们想要的数据。

但是还是建议把需要合并的列作为一个 非聚簇 索引来覆盖,效率要高。

七.表连接查询

首先就以最经典的学生、成绩来做示例吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
CREATE TABLE `student` (
`id` int(11) NOT NULL,
`name` varchar(50) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

BEGIN;
INSERT INTO `student` VALUES (1, '狗蛋');
INSERT INTO `student` VALUES (2, '狗剩');
INSERT INTO `student` VALUES (3, '翠花');
COMMIT;

DROP TABLE IF EXISTS `scope`;
CREATE TABLE `scope` (
`id` int(11) NOT NULL,
`subject` varchar(255) DEFAULT NULL,
`scope` int(255) DEFAULT NULL,
`stu_id` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

BEGIN;
INSERT INTO `scope` VALUES (1, '语文', 80, 1);
INSERT INTO `scope` VALUES (2, '数学', 98, 1);
INSERT INTO `scope` VALUES (3, '英语', 59, 2);
INSERT INTO `scope` VALUES (4, '政治', 79, 2);
COMMIT;

7.1 连表查询

那么先列举一个连表很普通的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
SELECT * 
FROM student, scope;

+----+--------+----+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+----+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 2 | 狗剩 | 1 | 语文 | 80 | 1 |
| 3 | 翠花 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 2 | 数学 | 98 | 1 |
| 3 | 翠花 | 2 | 数学 | 98 | 1 |
| 1 | 狗蛋 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 3 | 翠花 | 3 | 英语 | 59 | 2 |
| 1 | 狗蛋 | 4 | 政治 | 79 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
| 3 | 翠花 | 4 | 政治 | 79 | 2 |
+----+--------+----+---------+-------+--------+
12 rows in set (0.00 sec)

可以看到,如果我们不加任何条件,直接就连接两张表的话,取出来的结果就是第一张表所有的数据,去搭配第二张表所有的数据,也就是第一张表有 3 条数据,第二张表有 4 条记录,结果就有 3 ✖ 4 = 12 条记录。这个过程也称为 笛卡尔积。这种情况在日常编程中应该也没人会这么写的吧,仔细想想,如果两张表都是百万级别的数据,笛卡尔积是多恐怖的一件事情。

7.2 内连接

就是由于上面的结果集一般我们都不需要,所以我们会加上一些条件加以限制:

1
2
3
4
5
6
7
8
9
10
11
12
13
SELECT stu.*, scp.*
FROM student stu, scope scp
WHERE stu.id = scp.stu_id;

+----+--------+----+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+----+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
+----+--------+----+---------+-------+--------+
4 rows in set (0.00 sec)

这就是我们想要的结果集,所有参考学生的考试成绩。

InnoDB 是怎样的实现两个表连接的呢,这时候,优化器会根据查询成本确定 驱动表被驱动表

  • 驱动表:先查询的那个表,这里假设是 student 表,所以执行的时候,第一步执行 SELECT * FROM student;

  • 被驱动表:查询出来驱动表所有的数据以后,就会根据驱动表中每一条数据,去执行被驱动表。

    • 比如我现在 student 有三条记录,分别 id1 2 3
    • 然后对三条记录分别执行 SELECT * FROM scope WHERE stu_id = ?
    • 因为 scope 表中不存在 翠花 的成绩,所以 SELECT * FROM scope WHERE stu_id = 3 并没有展示出来,因为这个语句相当于 SELECT * FROM scope WHERE stu_id NOT NULL AND stu_id = 3

那如果上面的语句加上其他条件会怎么样,比如:

1
2
3
4
5
# 查询成绩 90 分以上的学生成绩信息
SELECT stu.*, scp.*
FROM student stu, scope scp
WHERE stu.id = scp.stu_id
AND scp.scope > 90;

那在访问驱动表 student 的时候同上面一样,但是针对驱动表中每一条数据执行被驱动表的时候,就会变成:

SELECT * FROM scope WHERE stu_id NOT NULL AND stu_id = ? AND scope > 90;

一句话就是说,这些条件针对哪个表,在执行那个表的时候就会带上指定的条件。

内连接推荐写法:

1
2
3
SELECT stu.*, scp.*
FROM student stu INNER JOIN scope scp ON stu.id = scp.stu_id
WHERE scp.scope > 90;

效率没有变高,但是可读性变好,在我看来,特定的限定功能就需要放在特定的位置,连接条件在 ON 后面,而查询条件在 WHERE 后面,也可以很好的跟下面的外连接写法统一。

7.3 外连接

外连接的目的是将连接在一起查询的表中,以某个表为主要目的,查询他在另外一个表的所有信息,包括不存在的信息。比如还是上面的例子,翠花她是个坏学生,一个科目都没有参加考试,但是老师如果通过上面的内连接查询来看的话,可能都不知道还有这个人的存在。这就麻烦大了啊,家长上门投诉,我给你钱了,她没参加考试你都不管管的吗。所以这时候,就需要外连接来做这个事情了:

1
2
3
4
5
6
7
8
9
10
11
12
SELECT * 
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id;
+----+--------+------+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+------+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
| 3 | 翠花 | NULL | NULL | NULL | NULL |
+----+--------+------+---------+-------+--------+
5 rows in set (0.00 sec)

这个时候,嗯哼,一抓一个准,没有来考试的,在 scope 表的字段中,显示为 NULL

关于 驱动表被驱动表 的含义已经在上面内连接的内容中指定,那么如果说 内连接 是根据成本来指定 驱动表 的,那 外连接 就是我们来指定驱动表。

驱动表 所有满足我们条件的数据,来查询 被驱动表,如果 被驱动表 不存在 驱动表 中某条数据的关联,显示为 NULL

那么关键点在哪里,就在这个 ON 后面的条件,ON 后面的条件,是指定 被驱动表 中不满足 ON 条件情况下依然要显示的关键。(在内连接中连接条件放 ON 和放 WHERE 效果一样)

如果外连接有几层,比如说三层:

1
2
3
4
SELECT *
FROM teacher t
LEFT JOIN student ON t.id = stu.into
LEFT JOIN scope scp ON stu.id = scope.stu_id;

那么上面这条查询需要怎么走呢,先查询老师所有的学生,得到一张中间表以后,再以这张中间表作为 驱动表 来查询 scope

八.复杂条件查询

MySQL InnoDB 中存在着 SQL解析器,解析器可谓为了查询正确的数据而费尽心思,目的只有一个,就是较快的查询出来正确的数据。其实这里也可以联想到 jvm编译器,为了执行效率比较快,会对字节码进行一些顺序的重新编排,什么 happen before 规则都出来了。

而什么东西查询最快呢,那就是查询常量的时候了。

8.1 整理查询语句

首先就是去除不必要的括号,因为括号如果没有意义存在,那移除了就更有利于优化器的解析了。

比如 WHERE (id = 1) 会被优化成 WHERE id = 1。这个好像也没什么好说的。

复杂一点的 WHERE (id = 1 AND name = 'WEIDAN') AND age = 20 则会被优化成 WHERE id = 1 AND name = 'WEIDAN' AND age = 20

8.2 简化查询条件

WHERE age > 18 AND real_age > age 则直接变成 WHERE age > 18 AND real_age > 18

8.3 删除废话条件

像我之前老师教的,在写 MyBatis Mapper.xml 的时候,因为 WHERE 总是需要动态的条件,所以前面会加一个 WHERE 1 = 1(为啥不用标签,因为标签可读性其实不怎么高)。

那么,如果我写的是 WHERE 1 = 1 AND name LIKE 'Weidan%',那么语句将会被直接优化为 WHERE name LIKE 'Weidan%' 。这么想想好像直接写 WHERE 1 = 1 除了解析器多了点工作,好像也没什么所谓了。

8.4 计算表达式

WHERE age = 9 + 9 那么解析器会直接给你个 WHERE age = 18。不过如果这些计算方式放在了列名上,比如:

1
2
WHERE -age = -18
WHERE to_days(age) = 18

解析器害怕他也优化错了,所以也会放弃优化,一个简单的记忆就是函数发生在等号后面的常量值上,可以优化,但是如果需要依赖列的计算的话,那就放弃优化了。

8.5 必要时转换外连接到内连接

右外连接会被转换为左外连接,毕竟 左外连接 更好确定 驱动表被驱动表

那如果我们在查询 左外连接 的时候,暗示着我得出的结果中,被驱动表不含 NULL 的值,聪明的暖男解析器将会 GET 到这个信息,把 外连接 优化成 内连接

比如上面的例子中:

1
2
3
4
5
6
7
SELECT * 
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id
AND scp.id = 1;

SELECT *
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id;
AND scp.id IS NOT NULL;

那么上面两个语句中是不是就都暗示解析器中,拿到的 scope 中的 id 不能为空。

所以!直接变成这条语句执行:

1
2
3
4
5
6
SELECT * 
FROM student stu INNER JOIN scope scp ON scp.stu_id = stu.id
AND scp.id = 1;

SELECT *
FROM student stu INNER JOIN scope scp ON scp.stu_id = stu.id;;

九.完

那长得丑怎么办,整容(旋转跳跃我闭着眼)啊,不过需要钱(性能)。

那我现在就按照 二叉查找树 那一颗,来插入一个 999,平衡二叉树是在增删改的时候通过旋转来平衡树。

一.InnoDB表数据

上面聊了这么多这个结构,那个结构的。现在是不是有点好奇,InnoDB 是把数据存在哪里的。答案也很简单,存在一颗 B+ 树种。

InnoDB 中,数据所在的位置没有其他地方,就只有一颗 B+ 树。而我们自己建立的索引,也是一颗 B+ 树。存储完整数据的 B+ 树也叫 聚簇索引,而我们自定义的列索引的 B+ 树,则称为 非聚簇索引

OK,大概明白了这两个东西以后,我们一个一个拆开来说说。

二.B+树是什么

关乎数据结构的东西了,如果觉得很简单,或者说已经学过脑袋有印象,可以跳过这段一个非计算机老男人写的废话。

B+ 树,一切要从一个最简单的例子开始,就是二分查找法。

2.1 二分查找法

我们在聚合的时候,估计都玩过一个游戏,叫做大冒险。确定谁来大冒险,有个方式就是猜数字:由上一轮中的倒霉鬼,来确定一个数字,然后围成一圈的小伙伴们来猜。猜中了,就是下一个倒霉鬼。

那怎么猜的呢,这个倒霉鬼在手机上输入一个数字,盖在地上防止修改。然后从一圈中某个人开始,说一个数字,那么那个站在台上的倒霉蛋就会缩小范围,让下一个人继续猜:

比如数字是69。

那么就有下面的对话:

1
2
3
4
5
6
A: 50
倒霉:50 - 100
B:75
倒霉:50 - 75
C:69
倒霉:恭喜你答对了balabala

这个过程咧,有没有很熟悉,对,就是 二分查找法


那为啥使用二分查找法呢,肯定是因为性能好啊。

那比如我有个 有序 数组:[18, 53, 55, 147, 151, 495, 551, 606, 638, 728]

我一次查询每一个数字,平均次数是 (1+2+3+4+5+6+7+8+9+10)/10 = 5.5次,而使用二分查找法则是 4+3+2+4+3+1+4+3+2+3)/10 = 2.9次。顺序查找最坏的情况也要 10 次,而二分查找最坏是 4 次。这么好的查找效率没有理由不用他。

2.2 二叉查找树

那二分查找和树什么关系,这里就要说到 二叉查找树,根据百度百科的定义:

一棵空树,或者是具有下列性质的二叉树

(1)若左子树不空,则左子树上所有结点的值均小于它的根结点的值;

(2)若右子树不空,则右子树上所有结点的值均大于它的根结点的值;

(3)左、右子树也分别为二叉排序树;

(4)没有键值相等的结点。

那么我现在把上面的数组转换成 二叉查找树,并且给予一个查找的动图:

查询看起来是很方便,但是对数据的增删查改就不是这样的了。有时候,数据插入多了,如果不调整可能会所有都放在了左边或者右边,这样性能就会越来越接近顺序查找。那么,数学家就提出了 平衡二叉树最优二叉树 的定义。

2.3 平衡二叉树

平衡二叉树:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1。这样子就可以让树变得矮胖矮胖的好身材,至于为啥矮胖矮胖才是好身材,因为这样子的话,查询效率才高啊。

比如下面这颗,查找大于 495 的时候,简直就是线性查找的效率了。

image-20200113155115465

那长得丑怎么办,整容(旋转跳跃我闭着眼)啊,不过需要钱(性能)。

那我现在就按照 二叉查找树 那一颗,来插入一个 999,平衡二叉树是在增删改的时候通过旋转来平衡树。

那么有长得帅的读者就要问了,这个跟 B+树 是什么关系。

2.4 B+树

因为 B+树 跟上面的思想差不多,所以,现在就来说说 B+树

B+树 她不是二叉树,但是查询跟 二叉树 差不多,我先放一颗 B+树

这是一颗高度为 2 扇出为 5B+树

那这个怎么实现查找的呢,现在比方说我要查找 606 这条数据:

那么 B+树 是怎么实现新增,然后平衡的呢,跟上面一样发生旋转。旋转的规则如下:

叶子节点是第二层的数据,索引节点是第一层查找时用到的数据。那旋转的情况就是节点页有没有满了:

叶子节点 索引节点 操作
× × 直接插入
× 1. 拆分叶子节点;
2. 中间节点值存入索引节点;
3. 小于中间节点的数据放左叶子,大于或等于放入右叶子。
1. 按照上面情况拆分叶子节点;
2. 然后根据相同的操作再拆分索引节点

第一种就不用演示了,比如新增 1000 直接存入最后边的叶子节点就好了。

接下来我演示第二种,那我就插入 645 这个数字吧。

可以从图中看到,拆分页是多么耗性能的一个东西了,所以我们经常说顺序插入是最好的性能,也要求主键一般呈递增的状态。

但是当左右兄弟节点可能没有满的时候,InnoDB不会急着去拆分数据页,而是会通过旋转的方式来让树达到平衡。这里就不说了。

那如果是第三种情况呢,拆分索引页跟拆分数据页是同样的道理,我就不画出来了,只要知道如果是第三种,性能还要比第二种低一倍,因为不仅拆分数据页,还拆分索引页了。

那如果页中有记录被删除呢,怎么去平衡,这时候就有一个东西叫做 计算因子,如果删除后的页的记录数量小于 计算因子*总页数 的时候,B+ 树会去做 合并操作。那我就继续用这一组数据来做示例。

删除 606 638 以后,B+ 树就会变成下面这个样子:

所以说,B+ 树在发生了修改以后,为了保证查询效率,会对某个一部分的节点进行整容。

三.InnoDB是怎么用B+树

好,结合上一篇的 InnoDB表结构 中的页和数据行的结构,以及刚刚所说的 B+树,现在我们就可以来看看一个表中的数据,是怎样被存储以及怎样被查询的。

从之前我们知道,数据是按照数据页的方式进行存放的,而数据页里面除了记录我们的用户记录行意外,还有一些额外的属性用来表示这一页的类型(所以数据页不仅仅是存放数据的页,也可能是 undo页 等等),而数据行里面也有一些额外的属性来辅助查找。

3.1 查找数据所在的页

之前已经系统的列举过 数据页数据行 的所有属性,现在我就挑在索引中需要用到的属性放在图中就可以了,放上其他的没什么用处,也占用地方。

那我现在就假设,上一节中的叶子节点中的数据(如下图),就是我们一条记录的主键 id,因为我们的记录肯定不止有主键这么简单,所以应该还有其他数据,我暂时使用一些占位的方框做代替。

那么这时候上面在数据在 MySQL 数据页中的结构,应该是这样子的:

叶子节点之间的关系,记录类型中:2 表示最小记录,3 表示最大记录,0 表示用户记录。那如果这棵树没有非叶子节点的话,搜索就是线性的需要一个一个遍历,这时候怎么办咧,B+树 的结构就出现了。

这个结构,就是上面 B+树InnoDB 中数据页的结构了。

3.2 查找真实数据

那么,上面我演示的一个页有三个记录,但其实不止三个记录,我就假设一个页有 16 条记录(其实不止,根据数据的大小)吧。那通过索引只能找到这一页呀,难道要一个一个遍历查询相对应的数据,这时候使用遍历还是有点早了(毕竟遍历并没有那么高效),所以在每个数据页中,又使用了一个槽进行分区。

那现在把镜头切换到一个页中来:

InnoDB 会把一个页中的所有数据根据一个很小的基数,比如 4 个记录为一组,去划分分组。那么每个组在数据页中都有对应的槽来记录分组的最大值。

那按照上面的数据,比如我要查询 55

槽0 = 18,槽1 = 54,槽2 = 64,槽3 = 77。

怎么找呢,类似于上面那个二分查找的游戏,槽018,老大说 18-77 之间,槽154,老大说 54-77,直到最后,确定数字是在 槽1槽2 之间,因为 槽2 = 64 大于 55,而 槽1 = 54 小于 55。所以从 槽1 的最大值 54 开始寻找下一条记录,遍历分组里面的数据行(因为这时候需要遍历的长度已经被切到很小了,所以是时候用遍历来做了,因为用其他算法的次数跟遍历差不多),取出 55 这条数据。

那整个查询过程中,需要跟硬盘交互的并不多,我们知道,InnoDB 是通过数据页来进行硬盘与内存的沟通的,查找效率高就意味着,我需要从硬盘取数据的次数越少。

3.3 删除记录

删除记录时,InnoDB 是怎么做的,就是在之前提到的数据行的头信息的 delete_flag 设置为 1,这条记录就会被放入 垃圾链表,当又需要插入新数据的时候,会判断是否合适放在这个位置,再把原来的数据给覆盖掉。

为什么这么做无非就是减少 硬盘IO。如果真的删除的话,需要去同步硬盘,这个过程并不是很重要,所以先标记就好了。

然后这条记录的上一条记录的 next_record 字段将被重新指向当前删除记录的下一条记录。

四.聚簇索引

那聚簇索引是什么,其实看了上面的图,加点料就可以了:

其实上面的例子我使用的是 id 来做例子,但是每条记录还带有其他所有列的信息(非 NULL)的,如果有 NULL 则会被记录在记录头信息,然后节省了存储数据的空间。

而这种索引,叫做 聚簇索引,意思是什么咧,就是一条记录保存了完整的数据信息,非聚簇则不是。而每条数据都会有一个 primary key,如果没有,InnoDB 会自动加上这个隐藏的列。这也是 MySQL_InnoDB 结构的库保存数据的方式。

所以可以这么说,InnoDB 就是 B+树 构成的。而根节点从建立表开始将会被记录,之后每次查询都会从这个根节点开始查询。

五.非聚簇索引

5.1 简单非聚簇索引

非聚簇索引聚簇索引 有什么区别,区别在于 非聚簇索引 并没有存放整条记录的所有数据,只是存放了 索引列主键 而已。所以当我们通过 非聚簇索引 查询一条数据行的所有列的时候,就需要 回表 去查询其他列的信息了,也就是说需要 两次查询 才完成这个需求。

比方一个表:

1
2
3
4
5
6
CREATE TABLE trbac_user (
id BIGINT auto_increase,
name varchar(50) NOT NULL,
mobile varchar(50) NOT NULL
PRIMARY KEY(id)
)

那我使用 name 来做非聚簇索引,这时候 InnoDB 就有一颗以 name 来查询的 B+树

那我们可以看到,数据行并没有覆盖所有数据,只有 name + id。那比如我们只是查询 name 或者 id 列,就可以直接在这里返回去了。

但是如果我还想要知道 mobile 列的话,这时候就需要在上面那棵树拿到 id,然后再去 id 那棵树查询其他的列信息。

需要从下面的主键的数据页来查询其他数据。

那这里有个什么经验呢…就是我们可以根据经常使用常用的列来建立索引,先拿到 id,然后再去缓存命中,再走数据库~

单列聚簇索引还有个需要注意的点,就是这个列的辨识度要搞,如果这个列只有 MANWOMAN 的话,为这个列建立索引根本就没有什么用处,因为跟全表扫描一样的效率,优化器更偏向于全表扫描,所以切记不要给列值只有寥寥几个的列建立索引,纯属浪费。

5.2 多列非聚簇索引

如果是 name + mobile 联合索引的话,需要注意的点就有点多了:

每个数据页的排序就会变成,先按照 name 进行排序,然后再按照 mobile 进行排序。

5.4 什么时候不会使用索引

1.不满足最左匹配

我想大家都听过 最左匹配原则,就是这个意思,因为我的索引是先根据 name 排序再根据 mobile 进行排序的。那么查询的时候,如果加上 order by 的话,这个排序同样也是派上用场的。

但是如果说查询条件只是写了 WHERE mobile = '13800x1',那上面的索引就完全派不上用场。

怎么说,因为我这个索引是结合了 name 先做排序然后索引的,你只是查询 mobile 的话,就需要遍历整个索引,拿到 id 后还需要回表,这个过程跟直接扫描 聚簇索引 相比,耗费的资源更多,所以,如果一个 name + mobile 的索引,只应用于带有 name 开始的查询条件。因为 name 在前面。这就是 最左匹配原则

2.排序一个列使用升序一个使用降序

比如 WHERE name like 'xxx%' ORDER BY name DESC,mobile ASC,这样的情况只能用到 name 列的索引(也就是所有 mobile 失去了效果),然后再把所有列重新整理,依据 mobile 重新排序。

如果两个都是同一个方向的排序就不会出现这种情况。

3.VARCHAR 匹配前缀查询

WHERE name like '%狗蛋' 这种情况为什么不能使用索引呢,因为你前面未知啊,根据 name 排序的索引查询跟全表扫描有什么区别,还少了 回表 的操作,所以只能全表扫描,取出满足 xxx狗蛋 的数据了。

4.排序用到索引中没有的列

这个理解了 区别在于 非聚簇索引 并没有存放整条记录的所有数据,只是存放了 索引列主键 而已 这句话,就知道为什么了,因为索引中的排序并不适合 SQL 指定的顺序。

5.使用函数修饰值的时候

比如 WHERE LOWER(name) = 'aa',每个列都需要通过函数调用,那就跟全表扫描没什么区别了。


那如果 SQL 语句中,条件出现的顺序没有和多列索引的顺序一致的话,会怎么样:

完全不必担心这个问题,因为 SQL 在执行之前还会交给一个叫做 优化器 的东西进行整理,下面会说到,现在就只要知道他会把 SQL 条件整理成跟索引一样的顺序就好了。

既然多列索引可以帮忙查询条件中满足最左匹配的 SQL 语句,是不是越多越好,答案并不是,我觉得应该根据权重来区分,因为 非聚簇索引 他的索引列并不会像自增 ID 的索引树一样,顺序的插入数据(历史原因导致我们项目使用 UUID 做主键的哭晕在厕所),而是经常的造成 页拆分(也就是上面拆分的动图),所以 非聚簇索引 多了,增删改 需要维护这棵树平衡所做的 整容 就越多,严重的话会影响 增删改 的效率。


5.5 什么时候命中索引

那什么时候用到了索引,自然就是避免上面的情况就可以了,就有这些情况,包括但不局限于:

  1. 条件覆盖了索引的最左列;
  2. 常量查询,WHERE name = 'xxx'
  3. 范围查询,满足 1 的情况下,比如 name > 'AA' 或者 name = 'AA' AND mobile > '13800';
  4. 排序,但是需要用到的排序列是同一个方向进行排序的。
  5. 分组,依然需要满足 1 条件,如果用到了 group by nameInnoDB 会先将相同的 name 放在一起,然后再继续根据其他条件进行运算。

5.6 回表代价

可以这么说,如果回表的代价过高,InnoDB解析器 可能直接决定,不要使用索引,直接全表扫描。

怎么回事,就是 非聚簇 索引他的值都是连在一起的,查询的时候,称为 顺序I/O,而 非聚簇索引 查询出来的 id 并不会连在一起,所以回表去查询 id 数据的时候,称为 随机I/O。而 随机I/O 的效率是很低的,所以当命中索引的数据行总是较多的时候,不如 全表扫描 来的快,所以可能出现 SQL 已经完全命中这个索引但是解析器他就是不使用。

怎么防止这种情况发生,就是让查询索引返回的数量少,比如我们可以 LIMIT 100 去限定只查询 100 条数据,或者我们只要查询索引中出现的列,比如 id 然后再在程序中,根据 id 列表去命中缓存,名不中的再一次性批量查库就可以了。

5.7 文件排序

有时候我们去解析 EXPLAIN 我们的 SQL 语句的时候,会出现 File Sort 类型,这个类型指的是,查询出来的结果不是按照我们 SQL 指定的顺序来的,所以需要在内存中(必要时借助硬盘)来重新对数据进行排序,这个过程就是 File Sort,那如何避免这个类型出现,那就是防止上面不命中索引的情况。

5.8 索引合并

使用等值且命中两个索引的列

比方说我的用户表有 name 索引 和 mobile 索引,那这个条件 WHERE name = 'Weidan' AND mobile = '18588777777' 就可以使用两个索引,然后取 id交集,然后回表查询。但是如果 name > 'Weidan' 这种条件,就不会使用到合并。

但是但是,如果是主键列是范围的话,是可以的,因为他们合并的时候也会操作主键嘛。

不过有没有使用到,还要看成本,如果一个索引命中的数据太多,依然不会使用合并索引。

合并两个索引的列

依然需要满足 列都是等值查询 这个条件,比如 WHERE name = 'Weidan' OR mobile = '18588777777',会合并两个主键再统一回表查询。

范围搜索合并查询

不过如果我们是范围查询,又想使用两个索引怎么办,我们可以把 name > 'Weidan' 以及 monile > '18588777777' 先查询出来 id 然后 union 在一起,再通过 id 去寻找我们想要的数据。

但是还是建议把需要合并的列作为一个 非聚簇 索引来覆盖,效率要高。

七.表连接查询

首先就以最经典的学生、成绩来做示例吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
CREATE TABLE `student` (
`id` int(11) NOT NULL,
`name` varchar(50) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

BEGIN;
INSERT INTO `student` VALUES (1, '狗蛋');
INSERT INTO `student` VALUES (2, '狗剩');
INSERT INTO `student` VALUES (3, '翠花');
COMMIT;

DROP TABLE IF EXISTS `scope`;
CREATE TABLE `scope` (
`id` int(11) NOT NULL,
`subject` varchar(255) DEFAULT NULL,
`scope` int(255) DEFAULT NULL,
`stu_id` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

BEGIN;
INSERT INTO `scope` VALUES (1, '语文', 80, 1);
INSERT INTO `scope` VALUES (2, '数学', 98, 1);
INSERT INTO `scope` VALUES (3, '英语', 59, 2);
INSERT INTO `scope` VALUES (4, '政治', 79, 2);
COMMIT;

7.1 连表查询

那么先列举一个连表很普通的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
SELECT * 
FROM student, scope;

+----+--------+----+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+----+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 2 | 狗剩 | 1 | 语文 | 80 | 1 |
| 3 | 翠花 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 2 | 数学 | 98 | 1 |
| 3 | 翠花 | 2 | 数学 | 98 | 1 |
| 1 | 狗蛋 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 3 | 翠花 | 3 | 英语 | 59 | 2 |
| 1 | 狗蛋 | 4 | 政治 | 79 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
| 3 | 翠花 | 4 | 政治 | 79 | 2 |
+----+--------+----+---------+-------+--------+
12 rows in set (0.00 sec)

可以看到,如果我们不加任何条件,直接就连接两张表的话,取出来的结果就是第一张表所有的数据,去搭配第二张表所有的数据,也就是第一张表有 3 条数据,第二张表有 4 条记录,结果就有 3 ✖ 4 = 12 条记录。这个过程也称为 笛卡尔积。这种情况在日常编程中应该也没人会这么写的吧,仔细想想,如果两张表都是百万级别的数据,笛卡尔积是多恐怖的一件事情。

7.2 内连接

就是由于上面的结果集一般我们都不需要,所以我们会加上一些条件加以限制:

1
2
3
4
5
6
7
8
9
10
11
12
13
SELECT stu.*, scp.*
FROM student stu, scope scp
WHERE stu.id = scp.stu_id;

+----+--------+----+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+----+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
+----+--------+----+---------+-------+--------+
4 rows in set (0.00 sec)

这就是我们想要的结果集,所有参考学生的考试成绩。

InnoDB 是怎样的实现两个表连接的呢,这时候,优化器会根据查询成本确定 驱动表被驱动表

  • 驱动表:先查询的那个表,这里假设是 student 表,所以执行的时候,第一步执行 SELECT * FROM student;

  • 被驱动表:查询出来驱动表所有的数据以后,就会根据驱动表中每一条数据,去执行被驱动表。

    • 比如我现在 student 有三条记录,分别 id1 2 3
    • 然后对三条记录分别执行 SELECT * FROM scope WHERE stu_id = ?
    • 因为 scope 表中不存在 翠花 的成绩,所以 SELECT * FROM scope WHERE stu_id = 3 并没有展示出来,因为这个语句相当于 SELECT * FROM scope WHERE stu_id NOT NULL AND stu_id = 3

那如果上面的语句加上其他条件会怎么样,比如:

1
2
3
4
5
# 查询成绩 90 分以上的学生成绩信息
SELECT stu.*, scp.*
FROM student stu, scope scp
WHERE stu.id = scp.stu_id
AND scp.scope > 90;

那在访问驱动表 student 的时候同上面一样,但是针对驱动表中每一条数据执行被驱动表的时候,就会变成:

SELECT * FROM scope WHERE stu_id NOT NULL AND stu_id = ? AND scope > 90;

一句话就是说,这些条件针对哪个表,在执行那个表的时候就会带上指定的条件。

内连接推荐写法:

1
2
3
SELECT stu.*, scp.*
FROM student stu INNER JOIN scope scp ON stu.id = scp.stu_id
WHERE scp.scope > 90;

效率没有变高,但是可读性变好,在我看来,特定的限定功能就需要放在特定的位置,连接条件在 ON 后面,而查询条件在 WHERE 后面,也可以很好的跟下面的外连接写法统一。

7.3 外连接

外连接的目的是将连接在一起查询的表中,以某个表为主要目的,查询他在另外一个表的所有信息,包括不存在的信息。比如还是上面的例子,翠花她是个坏学生,一个科目都没有参加考试,但是老师如果通过上面的内连接查询来看的话,可能都不知道还有这个人的存在。这就麻烦大了啊,家长上门投诉,我给你钱了,她没参加考试你都不管管的吗。所以这时候,就需要外连接来做这个事情了:

1
2
3
4
5
6
7
8
9
10
11
12
SELECT * 
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id;
+----+--------+------+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+------+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
| 3 | 翠花 | NULL | NULL | NULL | NULL |
+----+--------+------+---------+-------+--------+
5 rows in set (0.00 sec)

这个时候,嗯哼,一抓一个准,没有来考试的,在 scope 表的字段中,显示为 NULL

关于 驱动表被驱动表 的含义已经在上面内连接的内容中指定,那么如果说 内连接 是根据成本来指定 驱动表 的,那 外连接 就是我们来指定驱动表。

驱动表 所有满足我们条件的数据,来查询 被驱动表,如果 被驱动表 不存在 驱动表 中某条数据的关联,显示为 NULL

那么关键点在哪里,就在这个 ON 后面的条件,ON 后面的条件,是指定 被驱动表 中不满足 ON 条件情况下依然要显示的关键。(在内连接中连接条件放 ON 和放 WHERE 效果一样)

如果外连接有几层,比如说三层:

1
2
3
4
SELECT *
FROM teacher t
LEFT JOIN student ON t.id = stu.into
LEFT JOIN scope scp ON stu.id = scope.stu_id;

那么上面这条查询需要怎么走呢,先查询老师所有的学生,得到一张中间表以后,再以这张中间表作为 驱动表 来查询 scope

八.复杂条件查询

MySQL InnoDB 中存在着 SQL解析器,解析器可谓为了查询正确的数据而费尽心思,目的只有一个,就是较快的查询出来正确的数据。其实这里也可以联想到 jvm编译器,为了执行效率比较快,会对字节码进行一些顺序的重新编排,什么 happen before 规则都出来了。

而什么东西查询最快呢,那就是查询常量的时候了。

8.1 整理查询语句

首先就是去除不必要的括号,因为括号如果没有意义存在,那移除了就更有利于优化器的解析了。

比如 WHERE (id = 1) 会被优化成 WHERE id = 1。这个好像也没什么好说的。

复杂一点的 WHERE (id = 1 AND name = 'WEIDAN') AND age = 20 则会被优化成 WHERE id = 1 AND name = 'WEIDAN' AND age = 20

8.2 简化查询条件

WHERE age > 18 AND real_age > age 则直接变成 WHERE age > 18 AND real_age > 18

8.3 删除废话条件

像我之前老师教的,在写 MyBatis Mapper.xml 的时候,因为 WHERE 总是需要动态的条件,所以前面会加一个 WHERE 1 = 1(为啥不用标签,因为标签可读性其实不怎么高)。

那么,如果我写的是 WHERE 1 = 1 AND name LIKE 'Weidan%',那么语句将会被直接优化为 WHERE name LIKE 'Weidan%' 。这么想想好像直接写 WHERE 1 = 1 除了解析器多了点工作,好像也没什么所谓了。

8.4 计算表达式

WHERE age = 9 + 9 那么解析器会直接给你个 WHERE age = 18。不过如果这些计算方式放在了列名上,比如:

1
2
WHERE -age = -18
WHERE to_days(age) = 18

解析器害怕他也优化错了,所以也会放弃优化,一个简单的记忆就是函数发生在等号后面的常量值上,可以优化,但是如果需要依赖列的计算的话,那就放弃优化了。

8.5 必要时转换外连接到内连接

右外连接会被转换为左外连接,毕竟 左外连接 更好确定 驱动表被驱动表

那如果我们在查询 左外连接 的时候,暗示着我得出的结果中,被驱动表不含 NULL 的值,聪明的暖男解析器将会 GET 到这个信息,把 外连接 优化成 内连接

比如上面的例子中:

1
2
3
4
5
6
7
SELECT * 
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id
AND scp.id = 1;

SELECT *
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id;
AND scp.id IS NOT NULL;

那么上面两个语句中是不是就都暗示解析器中,拿到的 scope 中的 id 不能为空。

所以!直接变成这条语句执行:

1
2
3
4
5
6
SELECT * 
FROM student stu INNER JOIN scope scp ON scp.stu_id = stu.id
AND scp.id = 1;

SELECT *
FROM student stu INNER JOIN scope scp ON scp.stu_id = stu.id;;

九.完

那么有长得帅的读者就要问了,这个跟 B+树 是什么关系。

2.4 B+树

因为 B+树 跟上面的思想差不多,所以,现在就来说说 B+树

B+树 她不是二叉树,但是查询跟 二叉树 差不多,我先放一颗 B+树

一.InnoDB表数据

上面聊了这么多这个结构,那个结构的。现在是不是有点好奇,InnoDB 是把数据存在哪里的。答案也很简单,存在一颗 B+ 树种。

InnoDB 中,数据所在的位置没有其他地方,就只有一颗 B+ 树。而我们自己建立的索引,也是一颗 B+ 树。存储完整数据的 B+ 树也叫 聚簇索引,而我们自定义的列索引的 B+ 树,则称为 非聚簇索引

OK,大概明白了这两个东西以后,我们一个一个拆开来说说。

二.B+树是什么

关乎数据结构的东西了,如果觉得很简单,或者说已经学过脑袋有印象,可以跳过这段一个非计算机老男人写的废话。

B+ 树,一切要从一个最简单的例子开始,就是二分查找法。

2.1 二分查找法

我们在聚合的时候,估计都玩过一个游戏,叫做大冒险。确定谁来大冒险,有个方式就是猜数字:由上一轮中的倒霉鬼,来确定一个数字,然后围成一圈的小伙伴们来猜。猜中了,就是下一个倒霉鬼。

那怎么猜的呢,这个倒霉鬼在手机上输入一个数字,盖在地上防止修改。然后从一圈中某个人开始,说一个数字,那么那个站在台上的倒霉蛋就会缩小范围,让下一个人继续猜:

比如数字是69。

那么就有下面的对话:

1
2
3
4
5
6
A: 50
倒霉:50 - 100
B:75
倒霉:50 - 75
C:69
倒霉:恭喜你答对了balabala

这个过程咧,有没有很熟悉,对,就是 二分查找法


那为啥使用二分查找法呢,肯定是因为性能好啊。

那比如我有个 有序 数组:[18, 53, 55, 147, 151, 495, 551, 606, 638, 728]

我一次查询每一个数字,平均次数是 (1+2+3+4+5+6+7+8+9+10)/10 = 5.5次,而使用二分查找法则是 4+3+2+4+3+1+4+3+2+3)/10 = 2.9次。顺序查找最坏的情况也要 10 次,而二分查找最坏是 4 次。这么好的查找效率没有理由不用他。

2.2 二叉查找树

那二分查找和树什么关系,这里就要说到 二叉查找树,根据百度百科的定义:

一棵空树,或者是具有下列性质的二叉树

(1)若左子树不空,则左子树上所有结点的值均小于它的根结点的值;

(2)若右子树不空,则右子树上所有结点的值均大于它的根结点的值;

(3)左、右子树也分别为二叉排序树;

(4)没有键值相等的结点。

那么我现在把上面的数组转换成 二叉查找树,并且给予一个查找的动图:

查询看起来是很方便,但是对数据的增删查改就不是这样的了。有时候,数据插入多了,如果不调整可能会所有都放在了左边或者右边,这样性能就会越来越接近顺序查找。那么,数学家就提出了 平衡二叉树最优二叉树 的定义。

2.3 平衡二叉树

平衡二叉树:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1。这样子就可以让树变得矮胖矮胖的好身材,至于为啥矮胖矮胖才是好身材,因为这样子的话,查询效率才高啊。

比如下面这颗,查找大于 495 的时候,简直就是线性查找的效率了。

image-20200113155115465

那长得丑怎么办,整容(旋转跳跃我闭着眼)啊,不过需要钱(性能)。

那我现在就按照 二叉查找树 那一颗,来插入一个 999,平衡二叉树是在增删改的时候通过旋转来平衡树。

那么有长得帅的读者就要问了,这个跟 B+树 是什么关系。

2.4 B+树

因为 B+树 跟上面的思想差不多,所以,现在就来说说 B+树

B+树 她不是二叉树,但是查询跟 二叉树 差不多,我先放一颗 B+树

这是一颗高度为 2 扇出为 5B+树

那这个怎么实现查找的呢,现在比方说我要查找 606 这条数据:

那么 B+树 是怎么实现新增,然后平衡的呢,跟上面一样发生旋转。旋转的规则如下:

叶子节点是第二层的数据,索引节点是第一层查找时用到的数据。那旋转的情况就是节点页有没有满了:

叶子节点 索引节点 操作
× × 直接插入
× 1. 拆分叶子节点;
2. 中间节点值存入索引节点;
3. 小于中间节点的数据放左叶子,大于或等于放入右叶子。
1. 按照上面情况拆分叶子节点;
2. 然后根据相同的操作再拆分索引节点

第一种就不用演示了,比如新增 1000 直接存入最后边的叶子节点就好了。

接下来我演示第二种,那我就插入 645 这个数字吧。

可以从图中看到,拆分页是多么耗性能的一个东西了,所以我们经常说顺序插入是最好的性能,也要求主键一般呈递增的状态。

但是当左右兄弟节点可能没有满的时候,InnoDB不会急着去拆分数据页,而是会通过旋转的方式来让树达到平衡。这里就不说了。

那如果是第三种情况呢,拆分索引页跟拆分数据页是同样的道理,我就不画出来了,只要知道如果是第三种,性能还要比第二种低一倍,因为不仅拆分数据页,还拆分索引页了。

那如果页中有记录被删除呢,怎么去平衡,这时候就有一个东西叫做 计算因子,如果删除后的页的记录数量小于 计算因子*总页数 的时候,B+ 树会去做 合并操作。那我就继续用这一组数据来做示例。

删除 606 638 以后,B+ 树就会变成下面这个样子:

所以说,B+ 树在发生了修改以后,为了保证查询效率,会对某个一部分的节点进行整容。

三.InnoDB是怎么用B+树

好,结合上一篇的 InnoDB表结构 中的页和数据行的结构,以及刚刚所说的 B+树,现在我们就可以来看看一个表中的数据,是怎样被存储以及怎样被查询的。

从之前我们知道,数据是按照数据页的方式进行存放的,而数据页里面除了记录我们的用户记录行意外,还有一些额外的属性用来表示这一页的类型(所以数据页不仅仅是存放数据的页,也可能是 undo页 等等),而数据行里面也有一些额外的属性来辅助查找。

3.1 查找数据所在的页

之前已经系统的列举过 数据页数据行 的所有属性,现在我就挑在索引中需要用到的属性放在图中就可以了,放上其他的没什么用处,也占用地方。

那我现在就假设,上一节中的叶子节点中的数据(如下图),就是我们一条记录的主键 id,因为我们的记录肯定不止有主键这么简单,所以应该还有其他数据,我暂时使用一些占位的方框做代替。

那么这时候上面在数据在 MySQL 数据页中的结构,应该是这样子的:

叶子节点之间的关系,记录类型中:2 表示最小记录,3 表示最大记录,0 表示用户记录。那如果这棵树没有非叶子节点的话,搜索就是线性的需要一个一个遍历,这时候怎么办咧,B+树 的结构就出现了。

这个结构,就是上面 B+树InnoDB 中数据页的结构了。

3.2 查找真实数据

那么,上面我演示的一个页有三个记录,但其实不止三个记录,我就假设一个页有 16 条记录(其实不止,根据数据的大小)吧。那通过索引只能找到这一页呀,难道要一个一个遍历查询相对应的数据,这时候使用遍历还是有点早了(毕竟遍历并没有那么高效),所以在每个数据页中,又使用了一个槽进行分区。

那现在把镜头切换到一个页中来:

InnoDB 会把一个页中的所有数据根据一个很小的基数,比如 4 个记录为一组,去划分分组。那么每个组在数据页中都有对应的槽来记录分组的最大值。

那按照上面的数据,比如我要查询 55

槽0 = 18,槽1 = 54,槽2 = 64,槽3 = 77。

怎么找呢,类似于上面那个二分查找的游戏,槽018,老大说 18-77 之间,槽154,老大说 54-77,直到最后,确定数字是在 槽1槽2 之间,因为 槽2 = 64 大于 55,而 槽1 = 54 小于 55。所以从 槽1 的最大值 54 开始寻找下一条记录,遍历分组里面的数据行(因为这时候需要遍历的长度已经被切到很小了,所以是时候用遍历来做了,因为用其他算法的次数跟遍历差不多),取出 55 这条数据。

那整个查询过程中,需要跟硬盘交互的并不多,我们知道,InnoDB 是通过数据页来进行硬盘与内存的沟通的,查找效率高就意味着,我需要从硬盘取数据的次数越少。

3.3 删除记录

删除记录时,InnoDB 是怎么做的,就是在之前提到的数据行的头信息的 delete_flag 设置为 1,这条记录就会被放入 垃圾链表,当又需要插入新数据的时候,会判断是否合适放在这个位置,再把原来的数据给覆盖掉。

为什么这么做无非就是减少 硬盘IO。如果真的删除的话,需要去同步硬盘,这个过程并不是很重要,所以先标记就好了。

然后这条记录的上一条记录的 next_record 字段将被重新指向当前删除记录的下一条记录。

四.聚簇索引

那聚簇索引是什么,其实看了上面的图,加点料就可以了:

其实上面的例子我使用的是 id 来做例子,但是每条记录还带有其他所有列的信息(非 NULL)的,如果有 NULL 则会被记录在记录头信息,然后节省了存储数据的空间。

而这种索引,叫做 聚簇索引,意思是什么咧,就是一条记录保存了完整的数据信息,非聚簇则不是。而每条数据都会有一个 primary key,如果没有,InnoDB 会自动加上这个隐藏的列。这也是 MySQL_InnoDB 结构的库保存数据的方式。

所以可以这么说,InnoDB 就是 B+树 构成的。而根节点从建立表开始将会被记录,之后每次查询都会从这个根节点开始查询。

五.非聚簇索引

5.1 简单非聚簇索引

非聚簇索引聚簇索引 有什么区别,区别在于 非聚簇索引 并没有存放整条记录的所有数据,只是存放了 索引列主键 而已。所以当我们通过 非聚簇索引 查询一条数据行的所有列的时候,就需要 回表 去查询其他列的信息了,也就是说需要 两次查询 才完成这个需求。

比方一个表:

1
2
3
4
5
6
CREATE TABLE trbac_user (
id BIGINT auto_increase,
name varchar(50) NOT NULL,
mobile varchar(50) NOT NULL
PRIMARY KEY(id)
)

那我使用 name 来做非聚簇索引,这时候 InnoDB 就有一颗以 name 来查询的 B+树

那我们可以看到,数据行并没有覆盖所有数据,只有 name + id。那比如我们只是查询 name 或者 id 列,就可以直接在这里返回去了。

但是如果我还想要知道 mobile 列的话,这时候就需要在上面那棵树拿到 id,然后再去 id 那棵树查询其他的列信息。

需要从下面的主键的数据页来查询其他数据。

那这里有个什么经验呢…就是我们可以根据经常使用常用的列来建立索引,先拿到 id,然后再去缓存命中,再走数据库~

单列聚簇索引还有个需要注意的点,就是这个列的辨识度要搞,如果这个列只有 MANWOMAN 的话,为这个列建立索引根本就没有什么用处,因为跟全表扫描一样的效率,优化器更偏向于全表扫描,所以切记不要给列值只有寥寥几个的列建立索引,纯属浪费。

5.2 多列非聚簇索引

如果是 name + mobile 联合索引的话,需要注意的点就有点多了:

每个数据页的排序就会变成,先按照 name 进行排序,然后再按照 mobile 进行排序。

5.4 什么时候不会使用索引

1.不满足最左匹配

我想大家都听过 最左匹配原则,就是这个意思,因为我的索引是先根据 name 排序再根据 mobile 进行排序的。那么查询的时候,如果加上 order by 的话,这个排序同样也是派上用场的。

但是如果说查询条件只是写了 WHERE mobile = '13800x1',那上面的索引就完全派不上用场。

怎么说,因为我这个索引是结合了 name 先做排序然后索引的,你只是查询 mobile 的话,就需要遍历整个索引,拿到 id 后还需要回表,这个过程跟直接扫描 聚簇索引 相比,耗费的资源更多,所以,如果一个 name + mobile 的索引,只应用于带有 name 开始的查询条件。因为 name 在前面。这就是 最左匹配原则

2.排序一个列使用升序一个使用降序

比如 WHERE name like 'xxx%' ORDER BY name DESC,mobile ASC,这样的情况只能用到 name 列的索引(也就是所有 mobile 失去了效果),然后再把所有列重新整理,依据 mobile 重新排序。

如果两个都是同一个方向的排序就不会出现这种情况。

3.VARCHAR 匹配前缀查询

WHERE name like '%狗蛋' 这种情况为什么不能使用索引呢,因为你前面未知啊,根据 name 排序的索引查询跟全表扫描有什么区别,还少了 回表 的操作,所以只能全表扫描,取出满足 xxx狗蛋 的数据了。

4.排序用到索引中没有的列

这个理解了 区别在于 非聚簇索引 并没有存放整条记录的所有数据,只是存放了 索引列主键 而已 这句话,就知道为什么了,因为索引中的排序并不适合 SQL 指定的顺序。

5.使用函数修饰值的时候

比如 WHERE LOWER(name) = 'aa',每个列都需要通过函数调用,那就跟全表扫描没什么区别了。


那如果 SQL 语句中,条件出现的顺序没有和多列索引的顺序一致的话,会怎么样:

完全不必担心这个问题,因为 SQL 在执行之前还会交给一个叫做 优化器 的东西进行整理,下面会说到,现在就只要知道他会把 SQL 条件整理成跟索引一样的顺序就好了。

既然多列索引可以帮忙查询条件中满足最左匹配的 SQL 语句,是不是越多越好,答案并不是,我觉得应该根据权重来区分,因为 非聚簇索引 他的索引列并不会像自增 ID 的索引树一样,顺序的插入数据(历史原因导致我们项目使用 UUID 做主键的哭晕在厕所),而是经常的造成 页拆分(也就是上面拆分的动图),所以 非聚簇索引 多了,增删改 需要维护这棵树平衡所做的 整容 就越多,严重的话会影响 增删改 的效率。


5.5 什么时候命中索引

那什么时候用到了索引,自然就是避免上面的情况就可以了,就有这些情况,包括但不局限于:

  1. 条件覆盖了索引的最左列;
  2. 常量查询,WHERE name = 'xxx'
  3. 范围查询,满足 1 的情况下,比如 name > 'AA' 或者 name = 'AA' AND mobile > '13800';
  4. 排序,但是需要用到的排序列是同一个方向进行排序的。
  5. 分组,依然需要满足 1 条件,如果用到了 group by nameInnoDB 会先将相同的 name 放在一起,然后再继续根据其他条件进行运算。

5.6 回表代价

可以这么说,如果回表的代价过高,InnoDB解析器 可能直接决定,不要使用索引,直接全表扫描。

怎么回事,就是 非聚簇 索引他的值都是连在一起的,查询的时候,称为 顺序I/O,而 非聚簇索引 查询出来的 id 并不会连在一起,所以回表去查询 id 数据的时候,称为 随机I/O。而 随机I/O 的效率是很低的,所以当命中索引的数据行总是较多的时候,不如 全表扫描 来的快,所以可能出现 SQL 已经完全命中这个索引但是解析器他就是不使用。

怎么防止这种情况发生,就是让查询索引返回的数量少,比如我们可以 LIMIT 100 去限定只查询 100 条数据,或者我们只要查询索引中出现的列,比如 id 然后再在程序中,根据 id 列表去命中缓存,名不中的再一次性批量查库就可以了。

5.7 文件排序

有时候我们去解析 EXPLAIN 我们的 SQL 语句的时候,会出现 File Sort 类型,这个类型指的是,查询出来的结果不是按照我们 SQL 指定的顺序来的,所以需要在内存中(必要时借助硬盘)来重新对数据进行排序,这个过程就是 File Sort,那如何避免这个类型出现,那就是防止上面不命中索引的情况。

5.8 索引合并

使用等值且命中两个索引的列

比方说我的用户表有 name 索引 和 mobile 索引,那这个条件 WHERE name = 'Weidan' AND mobile = '18588777777' 就可以使用两个索引,然后取 id交集,然后回表查询。但是如果 name > 'Weidan' 这种条件,就不会使用到合并。

但是但是,如果是主键列是范围的话,是可以的,因为他们合并的时候也会操作主键嘛。

不过有没有使用到,还要看成本,如果一个索引命中的数据太多,依然不会使用合并索引。

合并两个索引的列

依然需要满足 列都是等值查询 这个条件,比如 WHERE name = 'Weidan' OR mobile = '18588777777',会合并两个主键再统一回表查询。

范围搜索合并查询

不过如果我们是范围查询,又想使用两个索引怎么办,我们可以把 name > 'Weidan' 以及 monile > '18588777777' 先查询出来 id 然后 union 在一起,再通过 id 去寻找我们想要的数据。

但是还是建议把需要合并的列作为一个 非聚簇 索引来覆盖,效率要高。

七.表连接查询

首先就以最经典的学生、成绩来做示例吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
CREATE TABLE `student` (
`id` int(11) NOT NULL,
`name` varchar(50) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

BEGIN;
INSERT INTO `student` VALUES (1, '狗蛋');
INSERT INTO `student` VALUES (2, '狗剩');
INSERT INTO `student` VALUES (3, '翠花');
COMMIT;

DROP TABLE IF EXISTS `scope`;
CREATE TABLE `scope` (
`id` int(11) NOT NULL,
`subject` varchar(255) DEFAULT NULL,
`scope` int(255) DEFAULT NULL,
`stu_id` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

BEGIN;
INSERT INTO `scope` VALUES (1, '语文', 80, 1);
INSERT INTO `scope` VALUES (2, '数学', 98, 1);
INSERT INTO `scope` VALUES (3, '英语', 59, 2);
INSERT INTO `scope` VALUES (4, '政治', 79, 2);
COMMIT;

7.1 连表查询

那么先列举一个连表很普通的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
SELECT * 
FROM student, scope;

+----+--------+----+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+----+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 2 | 狗剩 | 1 | 语文 | 80 | 1 |
| 3 | 翠花 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 2 | 数学 | 98 | 1 |
| 3 | 翠花 | 2 | 数学 | 98 | 1 |
| 1 | 狗蛋 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 3 | 翠花 | 3 | 英语 | 59 | 2 |
| 1 | 狗蛋 | 4 | 政治 | 79 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
| 3 | 翠花 | 4 | 政治 | 79 | 2 |
+----+--------+----+---------+-------+--------+
12 rows in set (0.00 sec)

可以看到,如果我们不加任何条件,直接就连接两张表的话,取出来的结果就是第一张表所有的数据,去搭配第二张表所有的数据,也就是第一张表有 3 条数据,第二张表有 4 条记录,结果就有 3 ✖ 4 = 12 条记录。这个过程也称为 笛卡尔积。这种情况在日常编程中应该也没人会这么写的吧,仔细想想,如果两张表都是百万级别的数据,笛卡尔积是多恐怖的一件事情。

7.2 内连接

就是由于上面的结果集一般我们都不需要,所以我们会加上一些条件加以限制:

1
2
3
4
5
6
7
8
9
10
11
12
13
SELECT stu.*, scp.*
FROM student stu, scope scp
WHERE stu.id = scp.stu_id;

+----+--------+----+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+----+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
+----+--------+----+---------+-------+--------+
4 rows in set (0.00 sec)

这就是我们想要的结果集,所有参考学生的考试成绩。

InnoDB 是怎样的实现两个表连接的呢,这时候,优化器会根据查询成本确定 驱动表被驱动表

  • 驱动表:先查询的那个表,这里假设是 student 表,所以执行的时候,第一步执行 SELECT * FROM student;

  • 被驱动表:查询出来驱动表所有的数据以后,就会根据驱动表中每一条数据,去执行被驱动表。

    • 比如我现在 student 有三条记录,分别 id1 2 3
    • 然后对三条记录分别执行 SELECT * FROM scope WHERE stu_id = ?
    • 因为 scope 表中不存在 翠花 的成绩,所以 SELECT * FROM scope WHERE stu_id = 3 并没有展示出来,因为这个语句相当于 SELECT * FROM scope WHERE stu_id NOT NULL AND stu_id = 3

那如果上面的语句加上其他条件会怎么样,比如:

1
2
3
4
5
# 查询成绩 90 分以上的学生成绩信息
SELECT stu.*, scp.*
FROM student stu, scope scp
WHERE stu.id = scp.stu_id
AND scp.scope > 90;

那在访问驱动表 student 的时候同上面一样,但是针对驱动表中每一条数据执行被驱动表的时候,就会变成:

SELECT * FROM scope WHERE stu_id NOT NULL AND stu_id = ? AND scope > 90;

一句话就是说,这些条件针对哪个表,在执行那个表的时候就会带上指定的条件。

内连接推荐写法:

1
2
3
SELECT stu.*, scp.*
FROM student stu INNER JOIN scope scp ON stu.id = scp.stu_id
WHERE scp.scope > 90;

效率没有变高,但是可读性变好,在我看来,特定的限定功能就需要放在特定的位置,连接条件在 ON 后面,而查询条件在 WHERE 后面,也可以很好的跟下面的外连接写法统一。

7.3 外连接

外连接的目的是将连接在一起查询的表中,以某个表为主要目的,查询他在另外一个表的所有信息,包括不存在的信息。比如还是上面的例子,翠花她是个坏学生,一个科目都没有参加考试,但是老师如果通过上面的内连接查询来看的话,可能都不知道还有这个人的存在。这就麻烦大了啊,家长上门投诉,我给你钱了,她没参加考试你都不管管的吗。所以这时候,就需要外连接来做这个事情了:

1
2
3
4
5
6
7
8
9
10
11
12
SELECT * 
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id;
+----+--------+------+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+------+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
| 3 | 翠花 | NULL | NULL | NULL | NULL |
+----+--------+------+---------+-------+--------+
5 rows in set (0.00 sec)

这个时候,嗯哼,一抓一个准,没有来考试的,在 scope 表的字段中,显示为 NULL

关于 驱动表被驱动表 的含义已经在上面内连接的内容中指定,那么如果说 内连接 是根据成本来指定 驱动表 的,那 外连接 就是我们来指定驱动表。

驱动表 所有满足我们条件的数据,来查询 被驱动表,如果 被驱动表 不存在 驱动表 中某条数据的关联,显示为 NULL

那么关键点在哪里,就在这个 ON 后面的条件,ON 后面的条件,是指定 被驱动表 中不满足 ON 条件情况下依然要显示的关键。(在内连接中连接条件放 ON 和放 WHERE 效果一样)

如果外连接有几层,比如说三层:

1
2
3
4
SELECT *
FROM teacher t
LEFT JOIN student ON t.id = stu.into
LEFT JOIN scope scp ON stu.id = scope.stu_id;

那么上面这条查询需要怎么走呢,先查询老师所有的学生,得到一张中间表以后,再以这张中间表作为 驱动表 来查询 scope

八.复杂条件查询

MySQL InnoDB 中存在着 SQL解析器,解析器可谓为了查询正确的数据而费尽心思,目的只有一个,就是较快的查询出来正确的数据。其实这里也可以联想到 jvm编译器,为了执行效率比较快,会对字节码进行一些顺序的重新编排,什么 happen before 规则都出来了。

而什么东西查询最快呢,那就是查询常量的时候了。

8.1 整理查询语句

首先就是去除不必要的括号,因为括号如果没有意义存在,那移除了就更有利于优化器的解析了。

比如 WHERE (id = 1) 会被优化成 WHERE id = 1。这个好像也没什么好说的。

复杂一点的 WHERE (id = 1 AND name = 'WEIDAN') AND age = 20 则会被优化成 WHERE id = 1 AND name = 'WEIDAN' AND age = 20

8.2 简化查询条件

WHERE age > 18 AND real_age > age 则直接变成 WHERE age > 18 AND real_age > 18

8.3 删除废话条件

像我之前老师教的,在写 MyBatis Mapper.xml 的时候,因为 WHERE 总是需要动态的条件,所以前面会加一个 WHERE 1 = 1(为啥不用标签,因为标签可读性其实不怎么高)。

那么,如果我写的是 WHERE 1 = 1 AND name LIKE 'Weidan%',那么语句将会被直接优化为 WHERE name LIKE 'Weidan%' 。这么想想好像直接写 WHERE 1 = 1 除了解析器多了点工作,好像也没什么所谓了。

8.4 计算表达式

WHERE age = 9 + 9 那么解析器会直接给你个 WHERE age = 18。不过如果这些计算方式放在了列名上,比如:

1
2
WHERE -age = -18
WHERE to_days(age) = 18

解析器害怕他也优化错了,所以也会放弃优化,一个简单的记忆就是函数发生在等号后面的常量值上,可以优化,但是如果需要依赖列的计算的话,那就放弃优化了。

8.5 必要时转换外连接到内连接

右外连接会被转换为左外连接,毕竟 左外连接 更好确定 驱动表被驱动表

那如果我们在查询 左外连接 的时候,暗示着我得出的结果中,被驱动表不含 NULL 的值,聪明的暖男解析器将会 GET 到这个信息,把 外连接 优化成 内连接

比如上面的例子中:

1
2
3
4
5
6
7
SELECT * 
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id
AND scp.id = 1;

SELECT *
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id;
AND scp.id IS NOT NULL;

那么上面两个语句中是不是就都暗示解析器中,拿到的 scope 中的 id 不能为空。

所以!直接变成这条语句执行:

1
2
3
4
5
6
SELECT * 
FROM student stu INNER JOIN scope scp ON scp.stu_id = stu.id
AND scp.id = 1;

SELECT *
FROM student stu INNER JOIN scope scp ON scp.stu_id = stu.id;;

九.完

这是一颗高度为 2 扇出为 5B+树

那这个怎么实现查找的呢,现在比方说我要查找 606 这条数据:

一.InnoDB表数据

上面聊了这么多这个结构,那个结构的。现在是不是有点好奇,InnoDB 是把数据存在哪里的。答案也很简单,存在一颗 B+ 树种。

InnoDB 中,数据所在的位置没有其他地方,就只有一颗 B+ 树。而我们自己建立的索引,也是一颗 B+ 树。存储完整数据的 B+ 树也叫 聚簇索引,而我们自定义的列索引的 B+ 树,则称为 非聚簇索引

OK,大概明白了这两个东西以后,我们一个一个拆开来说说。

二.B+树是什么

关乎数据结构的东西了,如果觉得很简单,或者说已经学过脑袋有印象,可以跳过这段一个非计算机老男人写的废话。

B+ 树,一切要从一个最简单的例子开始,就是二分查找法。

2.1 二分查找法

我们在聚合的时候,估计都玩过一个游戏,叫做大冒险。确定谁来大冒险,有个方式就是猜数字:由上一轮中的倒霉鬼,来确定一个数字,然后围成一圈的小伙伴们来猜。猜中了,就是下一个倒霉鬼。

那怎么猜的呢,这个倒霉鬼在手机上输入一个数字,盖在地上防止修改。然后从一圈中某个人开始,说一个数字,那么那个站在台上的倒霉蛋就会缩小范围,让下一个人继续猜:

比如数字是69。

那么就有下面的对话:

1
2
3
4
5
6
A: 50
倒霉:50 - 100
B:75
倒霉:50 - 75
C:69
倒霉:恭喜你答对了balabala

这个过程咧,有没有很熟悉,对,就是 二分查找法


那为啥使用二分查找法呢,肯定是因为性能好啊。

那比如我有个 有序 数组:[18, 53, 55, 147, 151, 495, 551, 606, 638, 728]

我一次查询每一个数字,平均次数是 (1+2+3+4+5+6+7+8+9+10)/10 = 5.5次,而使用二分查找法则是 4+3+2+4+3+1+4+3+2+3)/10 = 2.9次。顺序查找最坏的情况也要 10 次,而二分查找最坏是 4 次。这么好的查找效率没有理由不用他。

2.2 二叉查找树

那二分查找和树什么关系,这里就要说到 二叉查找树,根据百度百科的定义:

一棵空树,或者是具有下列性质的二叉树

(1)若左子树不空,则左子树上所有结点的值均小于它的根结点的值;

(2)若右子树不空,则右子树上所有结点的值均大于它的根结点的值;

(3)左、右子树也分别为二叉排序树;

(4)没有键值相等的结点。

那么我现在把上面的数组转换成 二叉查找树,并且给予一个查找的动图:

查询看起来是很方便,但是对数据的增删查改就不是这样的了。有时候,数据插入多了,如果不调整可能会所有都放在了左边或者右边,这样性能就会越来越接近顺序查找。那么,数学家就提出了 平衡二叉树最优二叉树 的定义。

2.3 平衡二叉树

平衡二叉树:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1。这样子就可以让树变得矮胖矮胖的好身材,至于为啥矮胖矮胖才是好身材,因为这样子的话,查询效率才高啊。

比如下面这颗,查找大于 495 的时候,简直就是线性查找的效率了。

image-20200113155115465

那长得丑怎么办,整容(旋转跳跃我闭着眼)啊,不过需要钱(性能)。

那我现在就按照 二叉查找树 那一颗,来插入一个 999,平衡二叉树是在增删改的时候通过旋转来平衡树。

那么有长得帅的读者就要问了,这个跟 B+树 是什么关系。

2.4 B+树

因为 B+树 跟上面的思想差不多,所以,现在就来说说 B+树

B+树 她不是二叉树,但是查询跟 二叉树 差不多,我先放一颗 B+树

这是一颗高度为 2 扇出为 5B+树

那这个怎么实现查找的呢,现在比方说我要查找 606 这条数据:

那么 B+树 是怎么实现新增,然后平衡的呢,跟上面一样发生旋转。旋转的规则如下:

叶子节点是第二层的数据,索引节点是第一层查找时用到的数据。那旋转的情况就是节点页有没有满了:

叶子节点 索引节点 操作
× × 直接插入
× 1. 拆分叶子节点;
2. 中间节点值存入索引节点;
3. 小于中间节点的数据放左叶子,大于或等于放入右叶子。
1. 按照上面情况拆分叶子节点;
2. 然后根据相同的操作再拆分索引节点

第一种就不用演示了,比如新增 1000 直接存入最后边的叶子节点就好了。

接下来我演示第二种,那我就插入 645 这个数字吧。

可以从图中看到,拆分页是多么耗性能的一个东西了,所以我们经常说顺序插入是最好的性能,也要求主键一般呈递增的状态。

但是当左右兄弟节点可能没有满的时候,InnoDB不会急着去拆分数据页,而是会通过旋转的方式来让树达到平衡。这里就不说了。

那如果是第三种情况呢,拆分索引页跟拆分数据页是同样的道理,我就不画出来了,只要知道如果是第三种,性能还要比第二种低一倍,因为不仅拆分数据页,还拆分索引页了。

那如果页中有记录被删除呢,怎么去平衡,这时候就有一个东西叫做 计算因子,如果删除后的页的记录数量小于 计算因子*总页数 的时候,B+ 树会去做 合并操作。那我就继续用这一组数据来做示例。

删除 606 638 以后,B+ 树就会变成下面这个样子:

所以说,B+ 树在发生了修改以后,为了保证查询效率,会对某个一部分的节点进行整容。

三.InnoDB是怎么用B+树

好,结合上一篇的 InnoDB表结构 中的页和数据行的结构,以及刚刚所说的 B+树,现在我们就可以来看看一个表中的数据,是怎样被存储以及怎样被查询的。

从之前我们知道,数据是按照数据页的方式进行存放的,而数据页里面除了记录我们的用户记录行意外,还有一些额外的属性用来表示这一页的类型(所以数据页不仅仅是存放数据的页,也可能是 undo页 等等),而数据行里面也有一些额外的属性来辅助查找。

3.1 查找数据所在的页

之前已经系统的列举过 数据页数据行 的所有属性,现在我就挑在索引中需要用到的属性放在图中就可以了,放上其他的没什么用处,也占用地方。

那我现在就假设,上一节中的叶子节点中的数据(如下图),就是我们一条记录的主键 id,因为我们的记录肯定不止有主键这么简单,所以应该还有其他数据,我暂时使用一些占位的方框做代替。

那么这时候上面在数据在 MySQL 数据页中的结构,应该是这样子的:

叶子节点之间的关系,记录类型中:2 表示最小记录,3 表示最大记录,0 表示用户记录。那如果这棵树没有非叶子节点的话,搜索就是线性的需要一个一个遍历,这时候怎么办咧,B+树 的结构就出现了。

这个结构,就是上面 B+树InnoDB 中数据页的结构了。

3.2 查找真实数据

那么,上面我演示的一个页有三个记录,但其实不止三个记录,我就假设一个页有 16 条记录(其实不止,根据数据的大小)吧。那通过索引只能找到这一页呀,难道要一个一个遍历查询相对应的数据,这时候使用遍历还是有点早了(毕竟遍历并没有那么高效),所以在每个数据页中,又使用了一个槽进行分区。

那现在把镜头切换到一个页中来:

InnoDB 会把一个页中的所有数据根据一个很小的基数,比如 4 个记录为一组,去划分分组。那么每个组在数据页中都有对应的槽来记录分组的最大值。

那按照上面的数据,比如我要查询 55

槽0 = 18,槽1 = 54,槽2 = 64,槽3 = 77。

怎么找呢,类似于上面那个二分查找的游戏,槽018,老大说 18-77 之间,槽154,老大说 54-77,直到最后,确定数字是在 槽1槽2 之间,因为 槽2 = 64 大于 55,而 槽1 = 54 小于 55。所以从 槽1 的最大值 54 开始寻找下一条记录,遍历分组里面的数据行(因为这时候需要遍历的长度已经被切到很小了,所以是时候用遍历来做了,因为用其他算法的次数跟遍历差不多),取出 55 这条数据。

那整个查询过程中,需要跟硬盘交互的并不多,我们知道,InnoDB 是通过数据页来进行硬盘与内存的沟通的,查找效率高就意味着,我需要从硬盘取数据的次数越少。

3.3 删除记录

删除记录时,InnoDB 是怎么做的,就是在之前提到的数据行的头信息的 delete_flag 设置为 1,这条记录就会被放入 垃圾链表,当又需要插入新数据的时候,会判断是否合适放在这个位置,再把原来的数据给覆盖掉。

为什么这么做无非就是减少 硬盘IO。如果真的删除的话,需要去同步硬盘,这个过程并不是很重要,所以先标记就好了。

然后这条记录的上一条记录的 next_record 字段将被重新指向当前删除记录的下一条记录。

四.聚簇索引

那聚簇索引是什么,其实看了上面的图,加点料就可以了:

其实上面的例子我使用的是 id 来做例子,但是每条记录还带有其他所有列的信息(非 NULL)的,如果有 NULL 则会被记录在记录头信息,然后节省了存储数据的空间。

而这种索引,叫做 聚簇索引,意思是什么咧,就是一条记录保存了完整的数据信息,非聚簇则不是。而每条数据都会有一个 primary key,如果没有,InnoDB 会自动加上这个隐藏的列。这也是 MySQL_InnoDB 结构的库保存数据的方式。

所以可以这么说,InnoDB 就是 B+树 构成的。而根节点从建立表开始将会被记录,之后每次查询都会从这个根节点开始查询。

五.非聚簇索引

5.1 简单非聚簇索引

非聚簇索引聚簇索引 有什么区别,区别在于 非聚簇索引 并没有存放整条记录的所有数据,只是存放了 索引列主键 而已。所以当我们通过 非聚簇索引 查询一条数据行的所有列的时候,就需要 回表 去查询其他列的信息了,也就是说需要 两次查询 才完成这个需求。

比方一个表:

1
2
3
4
5
6
CREATE TABLE trbac_user (
id BIGINT auto_increase,
name varchar(50) NOT NULL,
mobile varchar(50) NOT NULL
PRIMARY KEY(id)
)

那我使用 name 来做非聚簇索引,这时候 InnoDB 就有一颗以 name 来查询的 B+树

那我们可以看到,数据行并没有覆盖所有数据,只有 name + id。那比如我们只是查询 name 或者 id 列,就可以直接在这里返回去了。

但是如果我还想要知道 mobile 列的话,这时候就需要在上面那棵树拿到 id,然后再去 id 那棵树查询其他的列信息。

需要从下面的主键的数据页来查询其他数据。

那这里有个什么经验呢…就是我们可以根据经常使用常用的列来建立索引,先拿到 id,然后再去缓存命中,再走数据库~

单列聚簇索引还有个需要注意的点,就是这个列的辨识度要搞,如果这个列只有 MANWOMAN 的话,为这个列建立索引根本就没有什么用处,因为跟全表扫描一样的效率,优化器更偏向于全表扫描,所以切记不要给列值只有寥寥几个的列建立索引,纯属浪费。

5.2 多列非聚簇索引

如果是 name + mobile 联合索引的话,需要注意的点就有点多了:

每个数据页的排序就会变成,先按照 name 进行排序,然后再按照 mobile 进行排序。

5.4 什么时候不会使用索引

1.不满足最左匹配

我想大家都听过 最左匹配原则,就是这个意思,因为我的索引是先根据 name 排序再根据 mobile 进行排序的。那么查询的时候,如果加上 order by 的话,这个排序同样也是派上用场的。

但是如果说查询条件只是写了 WHERE mobile = '13800x1',那上面的索引就完全派不上用场。

怎么说,因为我这个索引是结合了 name 先做排序然后索引的,你只是查询 mobile 的话,就需要遍历整个索引,拿到 id 后还需要回表,这个过程跟直接扫描 聚簇索引 相比,耗费的资源更多,所以,如果一个 name + mobile 的索引,只应用于带有 name 开始的查询条件。因为 name 在前面。这就是 最左匹配原则

2.排序一个列使用升序一个使用降序

比如 WHERE name like 'xxx%' ORDER BY name DESC,mobile ASC,这样的情况只能用到 name 列的索引(也就是所有 mobile 失去了效果),然后再把所有列重新整理,依据 mobile 重新排序。

如果两个都是同一个方向的排序就不会出现这种情况。

3.VARCHAR 匹配前缀查询

WHERE name like '%狗蛋' 这种情况为什么不能使用索引呢,因为你前面未知啊,根据 name 排序的索引查询跟全表扫描有什么区别,还少了 回表 的操作,所以只能全表扫描,取出满足 xxx狗蛋 的数据了。

4.排序用到索引中没有的列

这个理解了 区别在于 非聚簇索引 并没有存放整条记录的所有数据,只是存放了 索引列主键 而已 这句话,就知道为什么了,因为索引中的排序并不适合 SQL 指定的顺序。

5.使用函数修饰值的时候

比如 WHERE LOWER(name) = 'aa',每个列都需要通过函数调用,那就跟全表扫描没什么区别了。


那如果 SQL 语句中,条件出现的顺序没有和多列索引的顺序一致的话,会怎么样:

完全不必担心这个问题,因为 SQL 在执行之前还会交给一个叫做 优化器 的东西进行整理,下面会说到,现在就只要知道他会把 SQL 条件整理成跟索引一样的顺序就好了。

既然多列索引可以帮忙查询条件中满足最左匹配的 SQL 语句,是不是越多越好,答案并不是,我觉得应该根据权重来区分,因为 非聚簇索引 他的索引列并不会像自增 ID 的索引树一样,顺序的插入数据(历史原因导致我们项目使用 UUID 做主键的哭晕在厕所),而是经常的造成 页拆分(也就是上面拆分的动图),所以 非聚簇索引 多了,增删改 需要维护这棵树平衡所做的 整容 就越多,严重的话会影响 增删改 的效率。


5.5 什么时候命中索引

那什么时候用到了索引,自然就是避免上面的情况就可以了,就有这些情况,包括但不局限于:

  1. 条件覆盖了索引的最左列;
  2. 常量查询,WHERE name = 'xxx'
  3. 范围查询,满足 1 的情况下,比如 name > 'AA' 或者 name = 'AA' AND mobile > '13800';
  4. 排序,但是需要用到的排序列是同一个方向进行排序的。
  5. 分组,依然需要满足 1 条件,如果用到了 group by nameInnoDB 会先将相同的 name 放在一起,然后再继续根据其他条件进行运算。

5.6 回表代价

可以这么说,如果回表的代价过高,InnoDB解析器 可能直接决定,不要使用索引,直接全表扫描。

怎么回事,就是 非聚簇 索引他的值都是连在一起的,查询的时候,称为 顺序I/O,而 非聚簇索引 查询出来的 id 并不会连在一起,所以回表去查询 id 数据的时候,称为 随机I/O。而 随机I/O 的效率是很低的,所以当命中索引的数据行总是较多的时候,不如 全表扫描 来的快,所以可能出现 SQL 已经完全命中这个索引但是解析器他就是不使用。

怎么防止这种情况发生,就是让查询索引返回的数量少,比如我们可以 LIMIT 100 去限定只查询 100 条数据,或者我们只要查询索引中出现的列,比如 id 然后再在程序中,根据 id 列表去命中缓存,名不中的再一次性批量查库就可以了。

5.7 文件排序

有时候我们去解析 EXPLAIN 我们的 SQL 语句的时候,会出现 File Sort 类型,这个类型指的是,查询出来的结果不是按照我们 SQL 指定的顺序来的,所以需要在内存中(必要时借助硬盘)来重新对数据进行排序,这个过程就是 File Sort,那如何避免这个类型出现,那就是防止上面不命中索引的情况。

5.8 索引合并

使用等值且命中两个索引的列

比方说我的用户表有 name 索引 和 mobile 索引,那这个条件 WHERE name = 'Weidan' AND mobile = '18588777777' 就可以使用两个索引,然后取 id交集,然后回表查询。但是如果 name > 'Weidan' 这种条件,就不会使用到合并。

但是但是,如果是主键列是范围的话,是可以的,因为他们合并的时候也会操作主键嘛。

不过有没有使用到,还要看成本,如果一个索引命中的数据太多,依然不会使用合并索引。

合并两个索引的列

依然需要满足 列都是等值查询 这个条件,比如 WHERE name = 'Weidan' OR mobile = '18588777777',会合并两个主键再统一回表查询。

范围搜索合并查询

不过如果我们是范围查询,又想使用两个索引怎么办,我们可以把 name > 'Weidan' 以及 monile > '18588777777' 先查询出来 id 然后 union 在一起,再通过 id 去寻找我们想要的数据。

但是还是建议把需要合并的列作为一个 非聚簇 索引来覆盖,效率要高。

七.表连接查询

首先就以最经典的学生、成绩来做示例吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
CREATE TABLE `student` (
`id` int(11) NOT NULL,
`name` varchar(50) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

BEGIN;
INSERT INTO `student` VALUES (1, '狗蛋');
INSERT INTO `student` VALUES (2, '狗剩');
INSERT INTO `student` VALUES (3, '翠花');
COMMIT;

DROP TABLE IF EXISTS `scope`;
CREATE TABLE `scope` (
`id` int(11) NOT NULL,
`subject` varchar(255) DEFAULT NULL,
`scope` int(255) DEFAULT NULL,
`stu_id` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

BEGIN;
INSERT INTO `scope` VALUES (1, '语文', 80, 1);
INSERT INTO `scope` VALUES (2, '数学', 98, 1);
INSERT INTO `scope` VALUES (3, '英语', 59, 2);
INSERT INTO `scope` VALUES (4, '政治', 79, 2);
COMMIT;

7.1 连表查询

那么先列举一个连表很普通的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
SELECT * 
FROM student, scope;

+----+--------+----+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+----+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 2 | 狗剩 | 1 | 语文 | 80 | 1 |
| 3 | 翠花 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 2 | 数学 | 98 | 1 |
| 3 | 翠花 | 2 | 数学 | 98 | 1 |
| 1 | 狗蛋 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 3 | 翠花 | 3 | 英语 | 59 | 2 |
| 1 | 狗蛋 | 4 | 政治 | 79 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
| 3 | 翠花 | 4 | 政治 | 79 | 2 |
+----+--------+----+---------+-------+--------+
12 rows in set (0.00 sec)

可以看到,如果我们不加任何条件,直接就连接两张表的话,取出来的结果就是第一张表所有的数据,去搭配第二张表所有的数据,也就是第一张表有 3 条数据,第二张表有 4 条记录,结果就有 3 ✖ 4 = 12 条记录。这个过程也称为 笛卡尔积。这种情况在日常编程中应该也没人会这么写的吧,仔细想想,如果两张表都是百万级别的数据,笛卡尔积是多恐怖的一件事情。

7.2 内连接

就是由于上面的结果集一般我们都不需要,所以我们会加上一些条件加以限制:

1
2
3
4
5
6
7
8
9
10
11
12
13
SELECT stu.*, scp.*
FROM student stu, scope scp
WHERE stu.id = scp.stu_id;

+----+--------+----+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+----+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
+----+--------+----+---------+-------+--------+
4 rows in set (0.00 sec)

这就是我们想要的结果集,所有参考学生的考试成绩。

InnoDB 是怎样的实现两个表连接的呢,这时候,优化器会根据查询成本确定 驱动表被驱动表

  • 驱动表:先查询的那个表,这里假设是 student 表,所以执行的时候,第一步执行 SELECT * FROM student;

  • 被驱动表:查询出来驱动表所有的数据以后,就会根据驱动表中每一条数据,去执行被驱动表。

    • 比如我现在 student 有三条记录,分别 id1 2 3
    • 然后对三条记录分别执行 SELECT * FROM scope WHERE stu_id = ?
    • 因为 scope 表中不存在 翠花 的成绩,所以 SELECT * FROM scope WHERE stu_id = 3 并没有展示出来,因为这个语句相当于 SELECT * FROM scope WHERE stu_id NOT NULL AND stu_id = 3

那如果上面的语句加上其他条件会怎么样,比如:

1
2
3
4
5
# 查询成绩 90 分以上的学生成绩信息
SELECT stu.*, scp.*
FROM student stu, scope scp
WHERE stu.id = scp.stu_id
AND scp.scope > 90;

那在访问驱动表 student 的时候同上面一样,但是针对驱动表中每一条数据执行被驱动表的时候,就会变成:

SELECT * FROM scope WHERE stu_id NOT NULL AND stu_id = ? AND scope > 90;

一句话就是说,这些条件针对哪个表,在执行那个表的时候就会带上指定的条件。

内连接推荐写法:

1
2
3
SELECT stu.*, scp.*
FROM student stu INNER JOIN scope scp ON stu.id = scp.stu_id
WHERE scp.scope > 90;

效率没有变高,但是可读性变好,在我看来,特定的限定功能就需要放在特定的位置,连接条件在 ON 后面,而查询条件在 WHERE 后面,也可以很好的跟下面的外连接写法统一。

7.3 外连接

外连接的目的是将连接在一起查询的表中,以某个表为主要目的,查询他在另外一个表的所有信息,包括不存在的信息。比如还是上面的例子,翠花她是个坏学生,一个科目都没有参加考试,但是老师如果通过上面的内连接查询来看的话,可能都不知道还有这个人的存在。这就麻烦大了啊,家长上门投诉,我给你钱了,她没参加考试你都不管管的吗。所以这时候,就需要外连接来做这个事情了:

1
2
3
4
5
6
7
8
9
10
11
12
SELECT * 
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id;
+----+--------+------+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+------+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
| 3 | 翠花 | NULL | NULL | NULL | NULL |
+----+--------+------+---------+-------+--------+
5 rows in set (0.00 sec)

这个时候,嗯哼,一抓一个准,没有来考试的,在 scope 表的字段中,显示为 NULL

关于 驱动表被驱动表 的含义已经在上面内连接的内容中指定,那么如果说 内连接 是根据成本来指定 驱动表 的,那 外连接 就是我们来指定驱动表。

驱动表 所有满足我们条件的数据,来查询 被驱动表,如果 被驱动表 不存在 驱动表 中某条数据的关联,显示为 NULL

那么关键点在哪里,就在这个 ON 后面的条件,ON 后面的条件,是指定 被驱动表 中不满足 ON 条件情况下依然要显示的关键。(在内连接中连接条件放 ON 和放 WHERE 效果一样)

如果外连接有几层,比如说三层:

1
2
3
4
SELECT *
FROM teacher t
LEFT JOIN student ON t.id = stu.into
LEFT JOIN scope scp ON stu.id = scope.stu_id;

那么上面这条查询需要怎么走呢,先查询老师所有的学生,得到一张中间表以后,再以这张中间表作为 驱动表 来查询 scope

八.复杂条件查询

MySQL InnoDB 中存在着 SQL解析器,解析器可谓为了查询正确的数据而费尽心思,目的只有一个,就是较快的查询出来正确的数据。其实这里也可以联想到 jvm编译器,为了执行效率比较快,会对字节码进行一些顺序的重新编排,什么 happen before 规则都出来了。

而什么东西查询最快呢,那就是查询常量的时候了。

8.1 整理查询语句

首先就是去除不必要的括号,因为括号如果没有意义存在,那移除了就更有利于优化器的解析了。

比如 WHERE (id = 1) 会被优化成 WHERE id = 1。这个好像也没什么好说的。

复杂一点的 WHERE (id = 1 AND name = 'WEIDAN') AND age = 20 则会被优化成 WHERE id = 1 AND name = 'WEIDAN' AND age = 20

8.2 简化查询条件

WHERE age > 18 AND real_age > age 则直接变成 WHERE age > 18 AND real_age > 18

8.3 删除废话条件

像我之前老师教的,在写 MyBatis Mapper.xml 的时候,因为 WHERE 总是需要动态的条件,所以前面会加一个 WHERE 1 = 1(为啥不用标签,因为标签可读性其实不怎么高)。

那么,如果我写的是 WHERE 1 = 1 AND name LIKE 'Weidan%',那么语句将会被直接优化为 WHERE name LIKE 'Weidan%' 。这么想想好像直接写 WHERE 1 = 1 除了解析器多了点工作,好像也没什么所谓了。

8.4 计算表达式

WHERE age = 9 + 9 那么解析器会直接给你个 WHERE age = 18。不过如果这些计算方式放在了列名上,比如:

1
2
WHERE -age = -18
WHERE to_days(age) = 18

解析器害怕他也优化错了,所以也会放弃优化,一个简单的记忆就是函数发生在等号后面的常量值上,可以优化,但是如果需要依赖列的计算的话,那就放弃优化了。

8.5 必要时转换外连接到内连接

右外连接会被转换为左外连接,毕竟 左外连接 更好确定 驱动表被驱动表

那如果我们在查询 左外连接 的时候,暗示着我得出的结果中,被驱动表不含 NULL 的值,聪明的暖男解析器将会 GET 到这个信息,把 外连接 优化成 内连接

比如上面的例子中:

1
2
3
4
5
6
7
SELECT * 
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id
AND scp.id = 1;

SELECT *
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id;
AND scp.id IS NOT NULL;

那么上面两个语句中是不是就都暗示解析器中,拿到的 scope 中的 id 不能为空。

所以!直接变成这条语句执行:

1
2
3
4
5
6
SELECT * 
FROM student stu INNER JOIN scope scp ON scp.stu_id = stu.id
AND scp.id = 1;

SELECT *
FROM student stu INNER JOIN scope scp ON scp.stu_id = stu.id;;

九.完

那么 B+树 是怎么实现新增,然后平衡的呢,跟上面一样发生旋转。旋转的规则如下:

叶子节点是第二层的数据,索引节点是第一层查找时用到的数据。那旋转的情况就是节点页有没有满了:

叶子节点 索引节点 操作
× × 直接插入
× 1. 拆分叶子节点;
2. 中间节点值存入索引节点;
3. 小于中间节点的数据放左叶子,大于或等于放入右叶子。
1. 按照上面情况拆分叶子节点;
2. 然后根据相同的操作再拆分索引节点

第一种就不用演示了,比如新增 1000 直接存入最后边的叶子节点就好了。

接下来我演示第二种,那我就插入 645 这个数字吧。

一.InnoDB表数据

上面聊了这么多这个结构,那个结构的。现在是不是有点好奇,InnoDB 是把数据存在哪里的。答案也很简单,存在一颗 B+ 树种。

InnoDB 中,数据所在的位置没有其他地方,就只有一颗 B+ 树。而我们自己建立的索引,也是一颗 B+ 树。存储完整数据的 B+ 树也叫 聚簇索引,而我们自定义的列索引的 B+ 树,则称为 非聚簇索引

OK,大概明白了这两个东西以后,我们一个一个拆开来说说。

二.B+树是什么

关乎数据结构的东西了,如果觉得很简单,或者说已经学过脑袋有印象,可以跳过这段一个非计算机老男人写的废话。

B+ 树,一切要从一个最简单的例子开始,就是二分查找法。

2.1 二分查找法

我们在聚合的时候,估计都玩过一个游戏,叫做大冒险。确定谁来大冒险,有个方式就是猜数字:由上一轮中的倒霉鬼,来确定一个数字,然后围成一圈的小伙伴们来猜。猜中了,就是下一个倒霉鬼。

那怎么猜的呢,这个倒霉鬼在手机上输入一个数字,盖在地上防止修改。然后从一圈中某个人开始,说一个数字,那么那个站在台上的倒霉蛋就会缩小范围,让下一个人继续猜:

比如数字是69。

那么就有下面的对话:

1
2
3
4
5
6
A: 50
倒霉:50 - 100
B:75
倒霉:50 - 75
C:69
倒霉:恭喜你答对了balabala

这个过程咧,有没有很熟悉,对,就是 二分查找法


那为啥使用二分查找法呢,肯定是因为性能好啊。

那比如我有个 有序 数组:[18, 53, 55, 147, 151, 495, 551, 606, 638, 728]

我一次查询每一个数字,平均次数是 (1+2+3+4+5+6+7+8+9+10)/10 = 5.5次,而使用二分查找法则是 4+3+2+4+3+1+4+3+2+3)/10 = 2.9次。顺序查找最坏的情况也要 10 次,而二分查找最坏是 4 次。这么好的查找效率没有理由不用他。

2.2 二叉查找树

那二分查找和树什么关系,这里就要说到 二叉查找树,根据百度百科的定义:

一棵空树,或者是具有下列性质的二叉树

(1)若左子树不空,则左子树上所有结点的值均小于它的根结点的值;

(2)若右子树不空,则右子树上所有结点的值均大于它的根结点的值;

(3)左、右子树也分别为二叉排序树;

(4)没有键值相等的结点。

那么我现在把上面的数组转换成 二叉查找树,并且给予一个查找的动图:

查询看起来是很方便,但是对数据的增删查改就不是这样的了。有时候,数据插入多了,如果不调整可能会所有都放在了左边或者右边,这样性能就会越来越接近顺序查找。那么,数学家就提出了 平衡二叉树最优二叉树 的定义。

2.3 平衡二叉树

平衡二叉树:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1。这样子就可以让树变得矮胖矮胖的好身材,至于为啥矮胖矮胖才是好身材,因为这样子的话,查询效率才高啊。

比如下面这颗,查找大于 495 的时候,简直就是线性查找的效率了。

image-20200113155115465

那长得丑怎么办,整容(旋转跳跃我闭着眼)啊,不过需要钱(性能)。

那我现在就按照 二叉查找树 那一颗,来插入一个 999,平衡二叉树是在增删改的时候通过旋转来平衡树。

那么有长得帅的读者就要问了,这个跟 B+树 是什么关系。

2.4 B+树

因为 B+树 跟上面的思想差不多,所以,现在就来说说 B+树

B+树 她不是二叉树,但是查询跟 二叉树 差不多,我先放一颗 B+树

这是一颗高度为 2 扇出为 5B+树

那这个怎么实现查找的呢,现在比方说我要查找 606 这条数据:

那么 B+树 是怎么实现新增,然后平衡的呢,跟上面一样发生旋转。旋转的规则如下:

叶子节点是第二层的数据,索引节点是第一层查找时用到的数据。那旋转的情况就是节点页有没有满了:

叶子节点 索引节点 操作
× × 直接插入
× 1. 拆分叶子节点;
2. 中间节点值存入索引节点;
3. 小于中间节点的数据放左叶子,大于或等于放入右叶子。
1. 按照上面情况拆分叶子节点;
2. 然后根据相同的操作再拆分索引节点

第一种就不用演示了,比如新增 1000 直接存入最后边的叶子节点就好了。

接下来我演示第二种,那我就插入 645 这个数字吧。

可以从图中看到,拆分页是多么耗性能的一个东西了,所以我们经常说顺序插入是最好的性能,也要求主键一般呈递增的状态。

但是当左右兄弟节点可能没有满的时候,InnoDB不会急着去拆分数据页,而是会通过旋转的方式来让树达到平衡。这里就不说了。

那如果是第三种情况呢,拆分索引页跟拆分数据页是同样的道理,我就不画出来了,只要知道如果是第三种,性能还要比第二种低一倍,因为不仅拆分数据页,还拆分索引页了。

那如果页中有记录被删除呢,怎么去平衡,这时候就有一个东西叫做 计算因子,如果删除后的页的记录数量小于 计算因子*总页数 的时候,B+ 树会去做 合并操作。那我就继续用这一组数据来做示例。

删除 606 638 以后,B+ 树就会变成下面这个样子:

所以说,B+ 树在发生了修改以后,为了保证查询效率,会对某个一部分的节点进行整容。

三.InnoDB是怎么用B+树

好,结合上一篇的 InnoDB表结构 中的页和数据行的结构,以及刚刚所说的 B+树,现在我们就可以来看看一个表中的数据,是怎样被存储以及怎样被查询的。

从之前我们知道,数据是按照数据页的方式进行存放的,而数据页里面除了记录我们的用户记录行意外,还有一些额外的属性用来表示这一页的类型(所以数据页不仅仅是存放数据的页,也可能是 undo页 等等),而数据行里面也有一些额外的属性来辅助查找。

3.1 查找数据所在的页

之前已经系统的列举过 数据页数据行 的所有属性,现在我就挑在索引中需要用到的属性放在图中就可以了,放上其他的没什么用处,也占用地方。

那我现在就假设,上一节中的叶子节点中的数据(如下图),就是我们一条记录的主键 id,因为我们的记录肯定不止有主键这么简单,所以应该还有其他数据,我暂时使用一些占位的方框做代替。

那么这时候上面在数据在 MySQL 数据页中的结构,应该是这样子的:

叶子节点之间的关系,记录类型中:2 表示最小记录,3 表示最大记录,0 表示用户记录。那如果这棵树没有非叶子节点的话,搜索就是线性的需要一个一个遍历,这时候怎么办咧,B+树 的结构就出现了。

这个结构,就是上面 B+树InnoDB 中数据页的结构了。

3.2 查找真实数据

那么,上面我演示的一个页有三个记录,但其实不止三个记录,我就假设一个页有 16 条记录(其实不止,根据数据的大小)吧。那通过索引只能找到这一页呀,难道要一个一个遍历查询相对应的数据,这时候使用遍历还是有点早了(毕竟遍历并没有那么高效),所以在每个数据页中,又使用了一个槽进行分区。

那现在把镜头切换到一个页中来:

InnoDB 会把一个页中的所有数据根据一个很小的基数,比如 4 个记录为一组,去划分分组。那么每个组在数据页中都有对应的槽来记录分组的最大值。

那按照上面的数据,比如我要查询 55

槽0 = 18,槽1 = 54,槽2 = 64,槽3 = 77。

怎么找呢,类似于上面那个二分查找的游戏,槽018,老大说 18-77 之间,槽154,老大说 54-77,直到最后,确定数字是在 槽1槽2 之间,因为 槽2 = 64 大于 55,而 槽1 = 54 小于 55。所以从 槽1 的最大值 54 开始寻找下一条记录,遍历分组里面的数据行(因为这时候需要遍历的长度已经被切到很小了,所以是时候用遍历来做了,因为用其他算法的次数跟遍历差不多),取出 55 这条数据。

那整个查询过程中,需要跟硬盘交互的并不多,我们知道,InnoDB 是通过数据页来进行硬盘与内存的沟通的,查找效率高就意味着,我需要从硬盘取数据的次数越少。

3.3 删除记录

删除记录时,InnoDB 是怎么做的,就是在之前提到的数据行的头信息的 delete_flag 设置为 1,这条记录就会被放入 垃圾链表,当又需要插入新数据的时候,会判断是否合适放在这个位置,再把原来的数据给覆盖掉。

为什么这么做无非就是减少 硬盘IO。如果真的删除的话,需要去同步硬盘,这个过程并不是很重要,所以先标记就好了。

然后这条记录的上一条记录的 next_record 字段将被重新指向当前删除记录的下一条记录。

四.聚簇索引

那聚簇索引是什么,其实看了上面的图,加点料就可以了:

其实上面的例子我使用的是 id 来做例子,但是每条记录还带有其他所有列的信息(非 NULL)的,如果有 NULL 则会被记录在记录头信息,然后节省了存储数据的空间。

而这种索引,叫做 聚簇索引,意思是什么咧,就是一条记录保存了完整的数据信息,非聚簇则不是。而每条数据都会有一个 primary key,如果没有,InnoDB 会自动加上这个隐藏的列。这也是 MySQL_InnoDB 结构的库保存数据的方式。

所以可以这么说,InnoDB 就是 B+树 构成的。而根节点从建立表开始将会被记录,之后每次查询都会从这个根节点开始查询。

五.非聚簇索引

5.1 简单非聚簇索引

非聚簇索引聚簇索引 有什么区别,区别在于 非聚簇索引 并没有存放整条记录的所有数据,只是存放了 索引列主键 而已。所以当我们通过 非聚簇索引 查询一条数据行的所有列的时候,就需要 回表 去查询其他列的信息了,也就是说需要 两次查询 才完成这个需求。

比方一个表:

1
2
3
4
5
6
CREATE TABLE trbac_user (
id BIGINT auto_increase,
name varchar(50) NOT NULL,
mobile varchar(50) NOT NULL
PRIMARY KEY(id)
)

那我使用 name 来做非聚簇索引,这时候 InnoDB 就有一颗以 name 来查询的 B+树

那我们可以看到,数据行并没有覆盖所有数据,只有 name + id。那比如我们只是查询 name 或者 id 列,就可以直接在这里返回去了。

但是如果我还想要知道 mobile 列的话,这时候就需要在上面那棵树拿到 id,然后再去 id 那棵树查询其他的列信息。

需要从下面的主键的数据页来查询其他数据。

那这里有个什么经验呢…就是我们可以根据经常使用常用的列来建立索引,先拿到 id,然后再去缓存命中,再走数据库~

单列聚簇索引还有个需要注意的点,就是这个列的辨识度要搞,如果这个列只有 MANWOMAN 的话,为这个列建立索引根本就没有什么用处,因为跟全表扫描一样的效率,优化器更偏向于全表扫描,所以切记不要给列值只有寥寥几个的列建立索引,纯属浪费。

5.2 多列非聚簇索引

如果是 name + mobile 联合索引的话,需要注意的点就有点多了:

每个数据页的排序就会变成,先按照 name 进行排序,然后再按照 mobile 进行排序。

5.4 什么时候不会使用索引

1.不满足最左匹配

我想大家都听过 最左匹配原则,就是这个意思,因为我的索引是先根据 name 排序再根据 mobile 进行排序的。那么查询的时候,如果加上 order by 的话,这个排序同样也是派上用场的。

但是如果说查询条件只是写了 WHERE mobile = '13800x1',那上面的索引就完全派不上用场。

怎么说,因为我这个索引是结合了 name 先做排序然后索引的,你只是查询 mobile 的话,就需要遍历整个索引,拿到 id 后还需要回表,这个过程跟直接扫描 聚簇索引 相比,耗费的资源更多,所以,如果一个 name + mobile 的索引,只应用于带有 name 开始的查询条件。因为 name 在前面。这就是 最左匹配原则

2.排序一个列使用升序一个使用降序

比如 WHERE name like 'xxx%' ORDER BY name DESC,mobile ASC,这样的情况只能用到 name 列的索引(也就是所有 mobile 失去了效果),然后再把所有列重新整理,依据 mobile 重新排序。

如果两个都是同一个方向的排序就不会出现这种情况。

3.VARCHAR 匹配前缀查询

WHERE name like '%狗蛋' 这种情况为什么不能使用索引呢,因为你前面未知啊,根据 name 排序的索引查询跟全表扫描有什么区别,还少了 回表 的操作,所以只能全表扫描,取出满足 xxx狗蛋 的数据了。

4.排序用到索引中没有的列

这个理解了 区别在于 非聚簇索引 并没有存放整条记录的所有数据,只是存放了 索引列主键 而已 这句话,就知道为什么了,因为索引中的排序并不适合 SQL 指定的顺序。

5.使用函数修饰值的时候

比如 WHERE LOWER(name) = 'aa',每个列都需要通过函数调用,那就跟全表扫描没什么区别了。


那如果 SQL 语句中,条件出现的顺序没有和多列索引的顺序一致的话,会怎么样:

完全不必担心这个问题,因为 SQL 在执行之前还会交给一个叫做 优化器 的东西进行整理,下面会说到,现在就只要知道他会把 SQL 条件整理成跟索引一样的顺序就好了。

既然多列索引可以帮忙查询条件中满足最左匹配的 SQL 语句,是不是越多越好,答案并不是,我觉得应该根据权重来区分,因为 非聚簇索引 他的索引列并不会像自增 ID 的索引树一样,顺序的插入数据(历史原因导致我们项目使用 UUID 做主键的哭晕在厕所),而是经常的造成 页拆分(也就是上面拆分的动图),所以 非聚簇索引 多了,增删改 需要维护这棵树平衡所做的 整容 就越多,严重的话会影响 增删改 的效率。


5.5 什么时候命中索引

那什么时候用到了索引,自然就是避免上面的情况就可以了,就有这些情况,包括但不局限于:

  1. 条件覆盖了索引的最左列;
  2. 常量查询,WHERE name = 'xxx'
  3. 范围查询,满足 1 的情况下,比如 name > 'AA' 或者 name = 'AA' AND mobile > '13800';
  4. 排序,但是需要用到的排序列是同一个方向进行排序的。
  5. 分组,依然需要满足 1 条件,如果用到了 group by nameInnoDB 会先将相同的 name 放在一起,然后再继续根据其他条件进行运算。

5.6 回表代价

可以这么说,如果回表的代价过高,InnoDB解析器 可能直接决定,不要使用索引,直接全表扫描。

怎么回事,就是 非聚簇 索引他的值都是连在一起的,查询的时候,称为 顺序I/O,而 非聚簇索引 查询出来的 id 并不会连在一起,所以回表去查询 id 数据的时候,称为 随机I/O。而 随机I/O 的效率是很低的,所以当命中索引的数据行总是较多的时候,不如 全表扫描 来的快,所以可能出现 SQL 已经完全命中这个索引但是解析器他就是不使用。

怎么防止这种情况发生,就是让查询索引返回的数量少,比如我们可以 LIMIT 100 去限定只查询 100 条数据,或者我们只要查询索引中出现的列,比如 id 然后再在程序中,根据 id 列表去命中缓存,名不中的再一次性批量查库就可以了。

5.7 文件排序

有时候我们去解析 EXPLAIN 我们的 SQL 语句的时候,会出现 File Sort 类型,这个类型指的是,查询出来的结果不是按照我们 SQL 指定的顺序来的,所以需要在内存中(必要时借助硬盘)来重新对数据进行排序,这个过程就是 File Sort,那如何避免这个类型出现,那就是防止上面不命中索引的情况。

5.8 索引合并

使用等值且命中两个索引的列

比方说我的用户表有 name 索引 和 mobile 索引,那这个条件 WHERE name = 'Weidan' AND mobile = '18588777777' 就可以使用两个索引,然后取 id交集,然后回表查询。但是如果 name > 'Weidan' 这种条件,就不会使用到合并。

但是但是,如果是主键列是范围的话,是可以的,因为他们合并的时候也会操作主键嘛。

不过有没有使用到,还要看成本,如果一个索引命中的数据太多,依然不会使用合并索引。

合并两个索引的列

依然需要满足 列都是等值查询 这个条件,比如 WHERE name = 'Weidan' OR mobile = '18588777777',会合并两个主键再统一回表查询。

范围搜索合并查询

不过如果我们是范围查询,又想使用两个索引怎么办,我们可以把 name > 'Weidan' 以及 monile > '18588777777' 先查询出来 id 然后 union 在一起,再通过 id 去寻找我们想要的数据。

但是还是建议把需要合并的列作为一个 非聚簇 索引来覆盖,效率要高。

七.表连接查询

首先就以最经典的学生、成绩来做示例吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
CREATE TABLE `student` (
`id` int(11) NOT NULL,
`name` varchar(50) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

BEGIN;
INSERT INTO `student` VALUES (1, '狗蛋');
INSERT INTO `student` VALUES (2, '狗剩');
INSERT INTO `student` VALUES (3, '翠花');
COMMIT;

DROP TABLE IF EXISTS `scope`;
CREATE TABLE `scope` (
`id` int(11) NOT NULL,
`subject` varchar(255) DEFAULT NULL,
`scope` int(255) DEFAULT NULL,
`stu_id` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

BEGIN;
INSERT INTO `scope` VALUES (1, '语文', 80, 1);
INSERT INTO `scope` VALUES (2, '数学', 98, 1);
INSERT INTO `scope` VALUES (3, '英语', 59, 2);
INSERT INTO `scope` VALUES (4, '政治', 79, 2);
COMMIT;

7.1 连表查询

那么先列举一个连表很普通的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
SELECT * 
FROM student, scope;

+----+--------+----+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+----+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 2 | 狗剩 | 1 | 语文 | 80 | 1 |
| 3 | 翠花 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 2 | 数学 | 98 | 1 |
| 3 | 翠花 | 2 | 数学 | 98 | 1 |
| 1 | 狗蛋 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 3 | 翠花 | 3 | 英语 | 59 | 2 |
| 1 | 狗蛋 | 4 | 政治 | 79 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
| 3 | 翠花 | 4 | 政治 | 79 | 2 |
+----+--------+----+---------+-------+--------+
12 rows in set (0.00 sec)

可以看到,如果我们不加任何条件,直接就连接两张表的话,取出来的结果就是第一张表所有的数据,去搭配第二张表所有的数据,也就是第一张表有 3 条数据,第二张表有 4 条记录,结果就有 3 ✖ 4 = 12 条记录。这个过程也称为 笛卡尔积。这种情况在日常编程中应该也没人会这么写的吧,仔细想想,如果两张表都是百万级别的数据,笛卡尔积是多恐怖的一件事情。

7.2 内连接

就是由于上面的结果集一般我们都不需要,所以我们会加上一些条件加以限制:

1
2
3
4
5
6
7
8
9
10
11
12
13
SELECT stu.*, scp.*
FROM student stu, scope scp
WHERE stu.id = scp.stu_id;

+----+--------+----+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+----+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
+----+--------+----+---------+-------+--------+
4 rows in set (0.00 sec)

这就是我们想要的结果集,所有参考学生的考试成绩。

InnoDB 是怎样的实现两个表连接的呢,这时候,优化器会根据查询成本确定 驱动表被驱动表

  • 驱动表:先查询的那个表,这里假设是 student 表,所以执行的时候,第一步执行 SELECT * FROM student;

  • 被驱动表:查询出来驱动表所有的数据以后,就会根据驱动表中每一条数据,去执行被驱动表。

    • 比如我现在 student 有三条记录,分别 id1 2 3
    • 然后对三条记录分别执行 SELECT * FROM scope WHERE stu_id = ?
    • 因为 scope 表中不存在 翠花 的成绩,所以 SELECT * FROM scope WHERE stu_id = 3 并没有展示出来,因为这个语句相当于 SELECT * FROM scope WHERE stu_id NOT NULL AND stu_id = 3

那如果上面的语句加上其他条件会怎么样,比如:

1
2
3
4
5
# 查询成绩 90 分以上的学生成绩信息
SELECT stu.*, scp.*
FROM student stu, scope scp
WHERE stu.id = scp.stu_id
AND scp.scope > 90;

那在访问驱动表 student 的时候同上面一样,但是针对驱动表中每一条数据执行被驱动表的时候,就会变成:

SELECT * FROM scope WHERE stu_id NOT NULL AND stu_id = ? AND scope > 90;

一句话就是说,这些条件针对哪个表,在执行那个表的时候就会带上指定的条件。

内连接推荐写法:

1
2
3
SELECT stu.*, scp.*
FROM student stu INNER JOIN scope scp ON stu.id = scp.stu_id
WHERE scp.scope > 90;

效率没有变高,但是可读性变好,在我看来,特定的限定功能就需要放在特定的位置,连接条件在 ON 后面,而查询条件在 WHERE 后面,也可以很好的跟下面的外连接写法统一。

7.3 外连接

外连接的目的是将连接在一起查询的表中,以某个表为主要目的,查询他在另外一个表的所有信息,包括不存在的信息。比如还是上面的例子,翠花她是个坏学生,一个科目都没有参加考试,但是老师如果通过上面的内连接查询来看的话,可能都不知道还有这个人的存在。这就麻烦大了啊,家长上门投诉,我给你钱了,她没参加考试你都不管管的吗。所以这时候,就需要外连接来做这个事情了:

1
2
3
4
5
6
7
8
9
10
11
12
SELECT * 
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id;
+----+--------+------+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+------+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
| 3 | 翠花 | NULL | NULL | NULL | NULL |
+----+--------+------+---------+-------+--------+
5 rows in set (0.00 sec)

这个时候,嗯哼,一抓一个准,没有来考试的,在 scope 表的字段中,显示为 NULL

关于 驱动表被驱动表 的含义已经在上面内连接的内容中指定,那么如果说 内连接 是根据成本来指定 驱动表 的,那 外连接 就是我们来指定驱动表。

驱动表 所有满足我们条件的数据,来查询 被驱动表,如果 被驱动表 不存在 驱动表 中某条数据的关联,显示为 NULL

那么关键点在哪里,就在这个 ON 后面的条件,ON 后面的条件,是指定 被驱动表 中不满足 ON 条件情况下依然要显示的关键。(在内连接中连接条件放 ON 和放 WHERE 效果一样)

如果外连接有几层,比如说三层:

1
2
3
4
SELECT *
FROM teacher t
LEFT JOIN student ON t.id = stu.into
LEFT JOIN scope scp ON stu.id = scope.stu_id;

那么上面这条查询需要怎么走呢,先查询老师所有的学生,得到一张中间表以后,再以这张中间表作为 驱动表 来查询 scope

八.复杂条件查询

MySQL InnoDB 中存在着 SQL解析器,解析器可谓为了查询正确的数据而费尽心思,目的只有一个,就是较快的查询出来正确的数据。其实这里也可以联想到 jvm编译器,为了执行效率比较快,会对字节码进行一些顺序的重新编排,什么 happen before 规则都出来了。

而什么东西查询最快呢,那就是查询常量的时候了。

8.1 整理查询语句

首先就是去除不必要的括号,因为括号如果没有意义存在,那移除了就更有利于优化器的解析了。

比如 WHERE (id = 1) 会被优化成 WHERE id = 1。这个好像也没什么好说的。

复杂一点的 WHERE (id = 1 AND name = 'WEIDAN') AND age = 20 则会被优化成 WHERE id = 1 AND name = 'WEIDAN' AND age = 20

8.2 简化查询条件

WHERE age > 18 AND real_age > age 则直接变成 WHERE age > 18 AND real_age > 18

8.3 删除废话条件

像我之前老师教的,在写 MyBatis Mapper.xml 的时候,因为 WHERE 总是需要动态的条件,所以前面会加一个 WHERE 1 = 1(为啥不用标签,因为标签可读性其实不怎么高)。

那么,如果我写的是 WHERE 1 = 1 AND name LIKE 'Weidan%',那么语句将会被直接优化为 WHERE name LIKE 'Weidan%' 。这么想想好像直接写 WHERE 1 = 1 除了解析器多了点工作,好像也没什么所谓了。

8.4 计算表达式

WHERE age = 9 + 9 那么解析器会直接给你个 WHERE age = 18。不过如果这些计算方式放在了列名上,比如:

1
2
WHERE -age = -18
WHERE to_days(age) = 18

解析器害怕他也优化错了,所以也会放弃优化,一个简单的记忆就是函数发生在等号后面的常量值上,可以优化,但是如果需要依赖列的计算的话,那就放弃优化了。

8.5 必要时转换外连接到内连接

右外连接会被转换为左外连接,毕竟 左外连接 更好确定 驱动表被驱动表

那如果我们在查询 左外连接 的时候,暗示着我得出的结果中,被驱动表不含 NULL 的值,聪明的暖男解析器将会 GET 到这个信息,把 外连接 优化成 内连接

比如上面的例子中:

1
2
3
4
5
6
7
SELECT * 
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id
AND scp.id = 1;

SELECT *
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id;
AND scp.id IS NOT NULL;

那么上面两个语句中是不是就都暗示解析器中,拿到的 scope 中的 id 不能为空。

所以!直接变成这条语句执行:

1
2
3
4
5
6
SELECT * 
FROM student stu INNER JOIN scope scp ON scp.stu_id = stu.id
AND scp.id = 1;

SELECT *
FROM student stu INNER JOIN scope scp ON scp.stu_id = stu.id;;

九.完

可以从图中看到,拆分页是多么耗性能的一个东西了,所以我们经常说顺序插入是最好的性能,也要求主键一般呈递增的状态。

但是当左右兄弟节点可能没有满的时候,InnoDB不会急着去拆分数据页,而是会通过旋转的方式来让树达到平衡。这里就不说了。

那如果是第三种情况呢,拆分索引页跟拆分数据页是同样的道理,我就不画出来了,只要知道如果是第三种,性能还要比第二种低一倍,因为不仅拆分数据页,还拆分索引页了。

那如果页中有记录被删除呢,怎么去平衡,这时候就有一个东西叫做 计算因子,如果删除后的页的记录数量小于 计算因子*总页数 的时候,B+ 树会去做 合并操作。那我就继续用这一组数据来做示例。

一.InnoDB表数据

上面聊了这么多这个结构,那个结构的。现在是不是有点好奇,InnoDB 是把数据存在哪里的。答案也很简单,存在一颗 B+ 树种。

InnoDB 中,数据所在的位置没有其他地方,就只有一颗 B+ 树。而我们自己建立的索引,也是一颗 B+ 树。存储完整数据的 B+ 树也叫 聚簇索引,而我们自定义的列索引的 B+ 树,则称为 非聚簇索引

OK,大概明白了这两个东西以后,我们一个一个拆开来说说。

二.B+树是什么

关乎数据结构的东西了,如果觉得很简单,或者说已经学过脑袋有印象,可以跳过这段一个非计算机老男人写的废话。

B+ 树,一切要从一个最简单的例子开始,就是二分查找法。

2.1 二分查找法

我们在聚合的时候,估计都玩过一个游戏,叫做大冒险。确定谁来大冒险,有个方式就是猜数字:由上一轮中的倒霉鬼,来确定一个数字,然后围成一圈的小伙伴们来猜。猜中了,就是下一个倒霉鬼。

那怎么猜的呢,这个倒霉鬼在手机上输入一个数字,盖在地上防止修改。然后从一圈中某个人开始,说一个数字,那么那个站在台上的倒霉蛋就会缩小范围,让下一个人继续猜:

比如数字是69。

那么就有下面的对话:

1
2
3
4
5
6
A: 50
倒霉:50 - 100
B:75
倒霉:50 - 75
C:69
倒霉:恭喜你答对了balabala

这个过程咧,有没有很熟悉,对,就是 二分查找法


那为啥使用二分查找法呢,肯定是因为性能好啊。

那比如我有个 有序 数组:[18, 53, 55, 147, 151, 495, 551, 606, 638, 728]

我一次查询每一个数字,平均次数是 (1+2+3+4+5+6+7+8+9+10)/10 = 5.5次,而使用二分查找法则是 4+3+2+4+3+1+4+3+2+3)/10 = 2.9次。顺序查找最坏的情况也要 10 次,而二分查找最坏是 4 次。这么好的查找效率没有理由不用他。

2.2 二叉查找树

那二分查找和树什么关系,这里就要说到 二叉查找树,根据百度百科的定义:

一棵空树,或者是具有下列性质的二叉树

(1)若左子树不空,则左子树上所有结点的值均小于它的根结点的值;

(2)若右子树不空,则右子树上所有结点的值均大于它的根结点的值;

(3)左、右子树也分别为二叉排序树;

(4)没有键值相等的结点。

那么我现在把上面的数组转换成 二叉查找树,并且给予一个查找的动图:

查询看起来是很方便,但是对数据的增删查改就不是这样的了。有时候,数据插入多了,如果不调整可能会所有都放在了左边或者右边,这样性能就会越来越接近顺序查找。那么,数学家就提出了 平衡二叉树最优二叉树 的定义。

2.3 平衡二叉树

平衡二叉树:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1。这样子就可以让树变得矮胖矮胖的好身材,至于为啥矮胖矮胖才是好身材,因为这样子的话,查询效率才高啊。

比如下面这颗,查找大于 495 的时候,简直就是线性查找的效率了。

image-20200113155115465

那长得丑怎么办,整容(旋转跳跃我闭着眼)啊,不过需要钱(性能)。

那我现在就按照 二叉查找树 那一颗,来插入一个 999,平衡二叉树是在增删改的时候通过旋转来平衡树。

那么有长得帅的读者就要问了,这个跟 B+树 是什么关系。

2.4 B+树

因为 B+树 跟上面的思想差不多,所以,现在就来说说 B+树

B+树 她不是二叉树,但是查询跟 二叉树 差不多,我先放一颗 B+树

这是一颗高度为 2 扇出为 5B+树

那这个怎么实现查找的呢,现在比方说我要查找 606 这条数据:

那么 B+树 是怎么实现新增,然后平衡的呢,跟上面一样发生旋转。旋转的规则如下:

叶子节点是第二层的数据,索引节点是第一层查找时用到的数据。那旋转的情况就是节点页有没有满了:

叶子节点 索引节点 操作
× × 直接插入
× 1. 拆分叶子节点;
2. 中间节点值存入索引节点;
3. 小于中间节点的数据放左叶子,大于或等于放入右叶子。
1. 按照上面情况拆分叶子节点;
2. 然后根据相同的操作再拆分索引节点

第一种就不用演示了,比如新增 1000 直接存入最后边的叶子节点就好了。

接下来我演示第二种,那我就插入 645 这个数字吧。

可以从图中看到,拆分页是多么耗性能的一个东西了,所以我们经常说顺序插入是最好的性能,也要求主键一般呈递增的状态。

但是当左右兄弟节点可能没有满的时候,InnoDB不会急着去拆分数据页,而是会通过旋转的方式来让树达到平衡。这里就不说了。

那如果是第三种情况呢,拆分索引页跟拆分数据页是同样的道理,我就不画出来了,只要知道如果是第三种,性能还要比第二种低一倍,因为不仅拆分数据页,还拆分索引页了。

那如果页中有记录被删除呢,怎么去平衡,这时候就有一个东西叫做 计算因子,如果删除后的页的记录数量小于 计算因子*总页数 的时候,B+ 树会去做 合并操作。那我就继续用这一组数据来做示例。

删除 606 638 以后,B+ 树就会变成下面这个样子:

所以说,B+ 树在发生了修改以后,为了保证查询效率,会对某个一部分的节点进行整容。

三.InnoDB是怎么用B+树

好,结合上一篇的 InnoDB表结构 中的页和数据行的结构,以及刚刚所说的 B+树,现在我们就可以来看看一个表中的数据,是怎样被存储以及怎样被查询的。

从之前我们知道,数据是按照数据页的方式进行存放的,而数据页里面除了记录我们的用户记录行意外,还有一些额外的属性用来表示这一页的类型(所以数据页不仅仅是存放数据的页,也可能是 undo页 等等),而数据行里面也有一些额外的属性来辅助查找。

3.1 查找数据所在的页

之前已经系统的列举过 数据页数据行 的所有属性,现在我就挑在索引中需要用到的属性放在图中就可以了,放上其他的没什么用处,也占用地方。

那我现在就假设,上一节中的叶子节点中的数据(如下图),就是我们一条记录的主键 id,因为我们的记录肯定不止有主键这么简单,所以应该还有其他数据,我暂时使用一些占位的方框做代替。

那么这时候上面在数据在 MySQL 数据页中的结构,应该是这样子的:

叶子节点之间的关系,记录类型中:2 表示最小记录,3 表示最大记录,0 表示用户记录。那如果这棵树没有非叶子节点的话,搜索就是线性的需要一个一个遍历,这时候怎么办咧,B+树 的结构就出现了。

这个结构,就是上面 B+树InnoDB 中数据页的结构了。

3.2 查找真实数据

那么,上面我演示的一个页有三个记录,但其实不止三个记录,我就假设一个页有 16 条记录(其实不止,根据数据的大小)吧。那通过索引只能找到这一页呀,难道要一个一个遍历查询相对应的数据,这时候使用遍历还是有点早了(毕竟遍历并没有那么高效),所以在每个数据页中,又使用了一个槽进行分区。

那现在把镜头切换到一个页中来:

InnoDB 会把一个页中的所有数据根据一个很小的基数,比如 4 个记录为一组,去划分分组。那么每个组在数据页中都有对应的槽来记录分组的最大值。

那按照上面的数据,比如我要查询 55

槽0 = 18,槽1 = 54,槽2 = 64,槽3 = 77。

怎么找呢,类似于上面那个二分查找的游戏,槽018,老大说 18-77 之间,槽154,老大说 54-77,直到最后,确定数字是在 槽1槽2 之间,因为 槽2 = 64 大于 55,而 槽1 = 54 小于 55。所以从 槽1 的最大值 54 开始寻找下一条记录,遍历分组里面的数据行(因为这时候需要遍历的长度已经被切到很小了,所以是时候用遍历来做了,因为用其他算法的次数跟遍历差不多),取出 55 这条数据。

那整个查询过程中,需要跟硬盘交互的并不多,我们知道,InnoDB 是通过数据页来进行硬盘与内存的沟通的,查找效率高就意味着,我需要从硬盘取数据的次数越少。

3.3 删除记录

删除记录时,InnoDB 是怎么做的,就是在之前提到的数据行的头信息的 delete_flag 设置为 1,这条记录就会被放入 垃圾链表,当又需要插入新数据的时候,会判断是否合适放在这个位置,再把原来的数据给覆盖掉。

为什么这么做无非就是减少 硬盘IO。如果真的删除的话,需要去同步硬盘,这个过程并不是很重要,所以先标记就好了。

然后这条记录的上一条记录的 next_record 字段将被重新指向当前删除记录的下一条记录。

四.聚簇索引

那聚簇索引是什么,其实看了上面的图,加点料就可以了:

其实上面的例子我使用的是 id 来做例子,但是每条记录还带有其他所有列的信息(非 NULL)的,如果有 NULL 则会被记录在记录头信息,然后节省了存储数据的空间。

而这种索引,叫做 聚簇索引,意思是什么咧,就是一条记录保存了完整的数据信息,非聚簇则不是。而每条数据都会有一个 primary key,如果没有,InnoDB 会自动加上这个隐藏的列。这也是 MySQL_InnoDB 结构的库保存数据的方式。

所以可以这么说,InnoDB 就是 B+树 构成的。而根节点从建立表开始将会被记录,之后每次查询都会从这个根节点开始查询。

五.非聚簇索引

5.1 简单非聚簇索引

非聚簇索引聚簇索引 有什么区别,区别在于 非聚簇索引 并没有存放整条记录的所有数据,只是存放了 索引列主键 而已。所以当我们通过 非聚簇索引 查询一条数据行的所有列的时候,就需要 回表 去查询其他列的信息了,也就是说需要 两次查询 才完成这个需求。

比方一个表:

1
2
3
4
5
6
CREATE TABLE trbac_user (
id BIGINT auto_increase,
name varchar(50) NOT NULL,
mobile varchar(50) NOT NULL
PRIMARY KEY(id)
)

那我使用 name 来做非聚簇索引,这时候 InnoDB 就有一颗以 name 来查询的 B+树

那我们可以看到,数据行并没有覆盖所有数据,只有 name + id。那比如我们只是查询 name 或者 id 列,就可以直接在这里返回去了。

但是如果我还想要知道 mobile 列的话,这时候就需要在上面那棵树拿到 id,然后再去 id 那棵树查询其他的列信息。

需要从下面的主键的数据页来查询其他数据。

那这里有个什么经验呢…就是我们可以根据经常使用常用的列来建立索引,先拿到 id,然后再去缓存命中,再走数据库~

单列聚簇索引还有个需要注意的点,就是这个列的辨识度要搞,如果这个列只有 MANWOMAN 的话,为这个列建立索引根本就没有什么用处,因为跟全表扫描一样的效率,优化器更偏向于全表扫描,所以切记不要给列值只有寥寥几个的列建立索引,纯属浪费。

5.2 多列非聚簇索引

如果是 name + mobile 联合索引的话,需要注意的点就有点多了:

每个数据页的排序就会变成,先按照 name 进行排序,然后再按照 mobile 进行排序。

5.4 什么时候不会使用索引

1.不满足最左匹配

我想大家都听过 最左匹配原则,就是这个意思,因为我的索引是先根据 name 排序再根据 mobile 进行排序的。那么查询的时候,如果加上 order by 的话,这个排序同样也是派上用场的。

但是如果说查询条件只是写了 WHERE mobile = '13800x1',那上面的索引就完全派不上用场。

怎么说,因为我这个索引是结合了 name 先做排序然后索引的,你只是查询 mobile 的话,就需要遍历整个索引,拿到 id 后还需要回表,这个过程跟直接扫描 聚簇索引 相比,耗费的资源更多,所以,如果一个 name + mobile 的索引,只应用于带有 name 开始的查询条件。因为 name 在前面。这就是 最左匹配原则

2.排序一个列使用升序一个使用降序

比如 WHERE name like 'xxx%' ORDER BY name DESC,mobile ASC,这样的情况只能用到 name 列的索引(也就是所有 mobile 失去了效果),然后再把所有列重新整理,依据 mobile 重新排序。

如果两个都是同一个方向的排序就不会出现这种情况。

3.VARCHAR 匹配前缀查询

WHERE name like '%狗蛋' 这种情况为什么不能使用索引呢,因为你前面未知啊,根据 name 排序的索引查询跟全表扫描有什么区别,还少了 回表 的操作,所以只能全表扫描,取出满足 xxx狗蛋 的数据了。

4.排序用到索引中没有的列

这个理解了 区别在于 非聚簇索引 并没有存放整条记录的所有数据,只是存放了 索引列主键 而已 这句话,就知道为什么了,因为索引中的排序并不适合 SQL 指定的顺序。

5.使用函数修饰值的时候

比如 WHERE LOWER(name) = 'aa',每个列都需要通过函数调用,那就跟全表扫描没什么区别了。


那如果 SQL 语句中,条件出现的顺序没有和多列索引的顺序一致的话,会怎么样:

完全不必担心这个问题,因为 SQL 在执行之前还会交给一个叫做 优化器 的东西进行整理,下面会说到,现在就只要知道他会把 SQL 条件整理成跟索引一样的顺序就好了。

既然多列索引可以帮忙查询条件中满足最左匹配的 SQL 语句,是不是越多越好,答案并不是,我觉得应该根据权重来区分,因为 非聚簇索引 他的索引列并不会像自增 ID 的索引树一样,顺序的插入数据(历史原因导致我们项目使用 UUID 做主键的哭晕在厕所),而是经常的造成 页拆分(也就是上面拆分的动图),所以 非聚簇索引 多了,增删改 需要维护这棵树平衡所做的 整容 就越多,严重的话会影响 增删改 的效率。


5.5 什么时候命中索引

那什么时候用到了索引,自然就是避免上面的情况就可以了,就有这些情况,包括但不局限于:

  1. 条件覆盖了索引的最左列;
  2. 常量查询,WHERE name = 'xxx'
  3. 范围查询,满足 1 的情况下,比如 name > 'AA' 或者 name = 'AA' AND mobile > '13800';
  4. 排序,但是需要用到的排序列是同一个方向进行排序的。
  5. 分组,依然需要满足 1 条件,如果用到了 group by nameInnoDB 会先将相同的 name 放在一起,然后再继续根据其他条件进行运算。

5.6 回表代价

可以这么说,如果回表的代价过高,InnoDB解析器 可能直接决定,不要使用索引,直接全表扫描。

怎么回事,就是 非聚簇 索引他的值都是连在一起的,查询的时候,称为 顺序I/O,而 非聚簇索引 查询出来的 id 并不会连在一起,所以回表去查询 id 数据的时候,称为 随机I/O。而 随机I/O 的效率是很低的,所以当命中索引的数据行总是较多的时候,不如 全表扫描 来的快,所以可能出现 SQL 已经完全命中这个索引但是解析器他就是不使用。

怎么防止这种情况发生,就是让查询索引返回的数量少,比如我们可以 LIMIT 100 去限定只查询 100 条数据,或者我们只要查询索引中出现的列,比如 id 然后再在程序中,根据 id 列表去命中缓存,名不中的再一次性批量查库就可以了。

5.7 文件排序

有时候我们去解析 EXPLAIN 我们的 SQL 语句的时候,会出现 File Sort 类型,这个类型指的是,查询出来的结果不是按照我们 SQL 指定的顺序来的,所以需要在内存中(必要时借助硬盘)来重新对数据进行排序,这个过程就是 File Sort,那如何避免这个类型出现,那就是防止上面不命中索引的情况。

5.8 索引合并

使用等值且命中两个索引的列

比方说我的用户表有 name 索引 和 mobile 索引,那这个条件 WHERE name = 'Weidan' AND mobile = '18588777777' 就可以使用两个索引,然后取 id交集,然后回表查询。但是如果 name > 'Weidan' 这种条件,就不会使用到合并。

但是但是,如果是主键列是范围的话,是可以的,因为他们合并的时候也会操作主键嘛。

不过有没有使用到,还要看成本,如果一个索引命中的数据太多,依然不会使用合并索引。

合并两个索引的列

依然需要满足 列都是等值查询 这个条件,比如 WHERE name = 'Weidan' OR mobile = '18588777777',会合并两个主键再统一回表查询。

范围搜索合并查询

不过如果我们是范围查询,又想使用两个索引怎么办,我们可以把 name > 'Weidan' 以及 monile > '18588777777' 先查询出来 id 然后 union 在一起,再通过 id 去寻找我们想要的数据。

但是还是建议把需要合并的列作为一个 非聚簇 索引来覆盖,效率要高。

七.表连接查询

首先就以最经典的学生、成绩来做示例吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
CREATE TABLE `student` (
`id` int(11) NOT NULL,
`name` varchar(50) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

BEGIN;
INSERT INTO `student` VALUES (1, '狗蛋');
INSERT INTO `student` VALUES (2, '狗剩');
INSERT INTO `student` VALUES (3, '翠花');
COMMIT;

DROP TABLE IF EXISTS `scope`;
CREATE TABLE `scope` (
`id` int(11) NOT NULL,
`subject` varchar(255) DEFAULT NULL,
`scope` int(255) DEFAULT NULL,
`stu_id` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

BEGIN;
INSERT INTO `scope` VALUES (1, '语文', 80, 1);
INSERT INTO `scope` VALUES (2, '数学', 98, 1);
INSERT INTO `scope` VALUES (3, '英语', 59, 2);
INSERT INTO `scope` VALUES (4, '政治', 79, 2);
COMMIT;

7.1 连表查询

那么先列举一个连表很普通的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
SELECT * 
FROM student, scope;

+----+--------+----+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+----+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 2 | 狗剩 | 1 | 语文 | 80 | 1 |
| 3 | 翠花 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 2 | 数学 | 98 | 1 |
| 3 | 翠花 | 2 | 数学 | 98 | 1 |
| 1 | 狗蛋 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 3 | 翠花 | 3 | 英语 | 59 | 2 |
| 1 | 狗蛋 | 4 | 政治 | 79 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
| 3 | 翠花 | 4 | 政治 | 79 | 2 |
+----+--------+----+---------+-------+--------+
12 rows in set (0.00 sec)

可以看到,如果我们不加任何条件,直接就连接两张表的话,取出来的结果就是第一张表所有的数据,去搭配第二张表所有的数据,也就是第一张表有 3 条数据,第二张表有 4 条记录,结果就有 3 ✖ 4 = 12 条记录。这个过程也称为 笛卡尔积。这种情况在日常编程中应该也没人会这么写的吧,仔细想想,如果两张表都是百万级别的数据,笛卡尔积是多恐怖的一件事情。

7.2 内连接

就是由于上面的结果集一般我们都不需要,所以我们会加上一些条件加以限制:

1
2
3
4
5
6
7
8
9
10
11
12
13
SELECT stu.*, scp.*
FROM student stu, scope scp
WHERE stu.id = scp.stu_id;

+----+--------+----+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+----+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
+----+--------+----+---------+-------+--------+
4 rows in set (0.00 sec)

这就是我们想要的结果集,所有参考学生的考试成绩。

InnoDB 是怎样的实现两个表连接的呢,这时候,优化器会根据查询成本确定 驱动表被驱动表

  • 驱动表:先查询的那个表,这里假设是 student 表,所以执行的时候,第一步执行 SELECT * FROM student;

  • 被驱动表:查询出来驱动表所有的数据以后,就会根据驱动表中每一条数据,去执行被驱动表。

    • 比如我现在 student 有三条记录,分别 id1 2 3
    • 然后对三条记录分别执行 SELECT * FROM scope WHERE stu_id = ?
    • 因为 scope 表中不存在 翠花 的成绩,所以 SELECT * FROM scope WHERE stu_id = 3 并没有展示出来,因为这个语句相当于 SELECT * FROM scope WHERE stu_id NOT NULL AND stu_id = 3

那如果上面的语句加上其他条件会怎么样,比如:

1
2
3
4
5
# 查询成绩 90 分以上的学生成绩信息
SELECT stu.*, scp.*
FROM student stu, scope scp
WHERE stu.id = scp.stu_id
AND scp.scope > 90;

那在访问驱动表 student 的时候同上面一样,但是针对驱动表中每一条数据执行被驱动表的时候,就会变成:

SELECT * FROM scope WHERE stu_id NOT NULL AND stu_id = ? AND scope > 90;

一句话就是说,这些条件针对哪个表,在执行那个表的时候就会带上指定的条件。

内连接推荐写法:

1
2
3
SELECT stu.*, scp.*
FROM student stu INNER JOIN scope scp ON stu.id = scp.stu_id
WHERE scp.scope > 90;

效率没有变高,但是可读性变好,在我看来,特定的限定功能就需要放在特定的位置,连接条件在 ON 后面,而查询条件在 WHERE 后面,也可以很好的跟下面的外连接写法统一。

7.3 外连接

外连接的目的是将连接在一起查询的表中,以某个表为主要目的,查询他在另外一个表的所有信息,包括不存在的信息。比如还是上面的例子,翠花她是个坏学生,一个科目都没有参加考试,但是老师如果通过上面的内连接查询来看的话,可能都不知道还有这个人的存在。这就麻烦大了啊,家长上门投诉,我给你钱了,她没参加考试你都不管管的吗。所以这时候,就需要外连接来做这个事情了:

1
2
3
4
5
6
7
8
9
10
11
12
SELECT * 
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id;
+----+--------+------+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+------+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
| 3 | 翠花 | NULL | NULL | NULL | NULL |
+----+--------+------+---------+-------+--------+
5 rows in set (0.00 sec)

这个时候,嗯哼,一抓一个准,没有来考试的,在 scope 表的字段中,显示为 NULL

关于 驱动表被驱动表 的含义已经在上面内连接的内容中指定,那么如果说 内连接 是根据成本来指定 驱动表 的,那 外连接 就是我们来指定驱动表。

驱动表 所有满足我们条件的数据,来查询 被驱动表,如果 被驱动表 不存在 驱动表 中某条数据的关联,显示为 NULL

那么关键点在哪里,就在这个 ON 后面的条件,ON 后面的条件,是指定 被驱动表 中不满足 ON 条件情况下依然要显示的关键。(在内连接中连接条件放 ON 和放 WHERE 效果一样)

如果外连接有几层,比如说三层:

1
2
3
4
SELECT *
FROM teacher t
LEFT JOIN student ON t.id = stu.into
LEFT JOIN scope scp ON stu.id = scope.stu_id;

那么上面这条查询需要怎么走呢,先查询老师所有的学生,得到一张中间表以后,再以这张中间表作为 驱动表 来查询 scope

八.复杂条件查询

MySQL InnoDB 中存在着 SQL解析器,解析器可谓为了查询正确的数据而费尽心思,目的只有一个,就是较快的查询出来正确的数据。其实这里也可以联想到 jvm编译器,为了执行效率比较快,会对字节码进行一些顺序的重新编排,什么 happen before 规则都出来了。

而什么东西查询最快呢,那就是查询常量的时候了。

8.1 整理查询语句

首先就是去除不必要的括号,因为括号如果没有意义存在,那移除了就更有利于优化器的解析了。

比如 WHERE (id = 1) 会被优化成 WHERE id = 1。这个好像也没什么好说的。

复杂一点的 WHERE (id = 1 AND name = 'WEIDAN') AND age = 20 则会被优化成 WHERE id = 1 AND name = 'WEIDAN' AND age = 20

8.2 简化查询条件

WHERE age > 18 AND real_age > age 则直接变成 WHERE age > 18 AND real_age > 18

8.3 删除废话条件

像我之前老师教的,在写 MyBatis Mapper.xml 的时候,因为 WHERE 总是需要动态的条件,所以前面会加一个 WHERE 1 = 1(为啥不用标签,因为标签可读性其实不怎么高)。

那么,如果我写的是 WHERE 1 = 1 AND name LIKE 'Weidan%',那么语句将会被直接优化为 WHERE name LIKE 'Weidan%' 。这么想想好像直接写 WHERE 1 = 1 除了解析器多了点工作,好像也没什么所谓了。

8.4 计算表达式

WHERE age = 9 + 9 那么解析器会直接给你个 WHERE age = 18。不过如果这些计算方式放在了列名上,比如:

1
2
WHERE -age = -18
WHERE to_days(age) = 18

解析器害怕他也优化错了,所以也会放弃优化,一个简单的记忆就是函数发生在等号后面的常量值上,可以优化,但是如果需要依赖列的计算的话,那就放弃优化了。

8.5 必要时转换外连接到内连接

右外连接会被转换为左外连接,毕竟 左外连接 更好确定 驱动表被驱动表

那如果我们在查询 左外连接 的时候,暗示着我得出的结果中,被驱动表不含 NULL 的值,聪明的暖男解析器将会 GET 到这个信息,把 外连接 优化成 内连接

比如上面的例子中:

1
2
3
4
5
6
7
SELECT * 
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id
AND scp.id = 1;

SELECT *
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id;
AND scp.id IS NOT NULL;

那么上面两个语句中是不是就都暗示解析器中,拿到的 scope 中的 id 不能为空。

所以!直接变成这条语句执行:

1
2
3
4
5
6
SELECT * 
FROM student stu INNER JOIN scope scp ON scp.stu_id = stu.id
AND scp.id = 1;

SELECT *
FROM student stu INNER JOIN scope scp ON scp.stu_id = stu.id;;

九.完

删除 606 638 以后,B+ 树就会变成下面这个样子:

一.InnoDB表数据

上面聊了这么多这个结构,那个结构的。现在是不是有点好奇,InnoDB 是把数据存在哪里的。答案也很简单,存在一颗 B+ 树种。

InnoDB 中,数据所在的位置没有其他地方,就只有一颗 B+ 树。而我们自己建立的索引,也是一颗 B+ 树。存储完整数据的 B+ 树也叫 聚簇索引,而我们自定义的列索引的 B+ 树,则称为 非聚簇索引

OK,大概明白了这两个东西以后,我们一个一个拆开来说说。

二.B+树是什么

关乎数据结构的东西了,如果觉得很简单,或者说已经学过脑袋有印象,可以跳过这段一个非计算机老男人写的废话。

B+ 树,一切要从一个最简单的例子开始,就是二分查找法。

2.1 二分查找法

我们在聚合的时候,估计都玩过一个游戏,叫做大冒险。确定谁来大冒险,有个方式就是猜数字:由上一轮中的倒霉鬼,来确定一个数字,然后围成一圈的小伙伴们来猜。猜中了,就是下一个倒霉鬼。

那怎么猜的呢,这个倒霉鬼在手机上输入一个数字,盖在地上防止修改。然后从一圈中某个人开始,说一个数字,那么那个站在台上的倒霉蛋就会缩小范围,让下一个人继续猜:

比如数字是69。

那么就有下面的对话:

1
2
3
4
5
6
A: 50
倒霉:50 - 100
B:75
倒霉:50 - 75
C:69
倒霉:恭喜你答对了balabala

这个过程咧,有没有很熟悉,对,就是 二分查找法


那为啥使用二分查找法呢,肯定是因为性能好啊。

那比如我有个 有序 数组:[18, 53, 55, 147, 151, 495, 551, 606, 638, 728]

我一次查询每一个数字,平均次数是 (1+2+3+4+5+6+7+8+9+10)/10 = 5.5次,而使用二分查找法则是 4+3+2+4+3+1+4+3+2+3)/10 = 2.9次。顺序查找最坏的情况也要 10 次,而二分查找最坏是 4 次。这么好的查找效率没有理由不用他。

2.2 二叉查找树

那二分查找和树什么关系,这里就要说到 二叉查找树,根据百度百科的定义:

一棵空树,或者是具有下列性质的二叉树

(1)若左子树不空,则左子树上所有结点的值均小于它的根结点的值;

(2)若右子树不空,则右子树上所有结点的值均大于它的根结点的值;

(3)左、右子树也分别为二叉排序树;

(4)没有键值相等的结点。

那么我现在把上面的数组转换成 二叉查找树,并且给予一个查找的动图:

查询看起来是很方便,但是对数据的增删查改就不是这样的了。有时候,数据插入多了,如果不调整可能会所有都放在了左边或者右边,这样性能就会越来越接近顺序查找。那么,数学家就提出了 平衡二叉树最优二叉树 的定义。

2.3 平衡二叉树

平衡二叉树:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1。这样子就可以让树变得矮胖矮胖的好身材,至于为啥矮胖矮胖才是好身材,因为这样子的话,查询效率才高啊。

比如下面这颗,查找大于 495 的时候,简直就是线性查找的效率了。

image-20200113155115465

那长得丑怎么办,整容(旋转跳跃我闭着眼)啊,不过需要钱(性能)。

那我现在就按照 二叉查找树 那一颗,来插入一个 999,平衡二叉树是在增删改的时候通过旋转来平衡树。

那么有长得帅的读者就要问了,这个跟 B+树 是什么关系。

2.4 B+树

因为 B+树 跟上面的思想差不多,所以,现在就来说说 B+树

B+树 她不是二叉树,但是查询跟 二叉树 差不多,我先放一颗 B+树

这是一颗高度为 2 扇出为 5B+树

那这个怎么实现查找的呢,现在比方说我要查找 606 这条数据:

那么 B+树 是怎么实现新增,然后平衡的呢,跟上面一样发生旋转。旋转的规则如下:

叶子节点是第二层的数据,索引节点是第一层查找时用到的数据。那旋转的情况就是节点页有没有满了:

叶子节点 索引节点 操作
× × 直接插入
× 1. 拆分叶子节点;
2. 中间节点值存入索引节点;
3. 小于中间节点的数据放左叶子,大于或等于放入右叶子。
1. 按照上面情况拆分叶子节点;
2. 然后根据相同的操作再拆分索引节点

第一种就不用演示了,比如新增 1000 直接存入最后边的叶子节点就好了。

接下来我演示第二种,那我就插入 645 这个数字吧。

可以从图中看到,拆分页是多么耗性能的一个东西了,所以我们经常说顺序插入是最好的性能,也要求主键一般呈递增的状态。

但是当左右兄弟节点可能没有满的时候,InnoDB不会急着去拆分数据页,而是会通过旋转的方式来让树达到平衡。这里就不说了。

那如果是第三种情况呢,拆分索引页跟拆分数据页是同样的道理,我就不画出来了,只要知道如果是第三种,性能还要比第二种低一倍,因为不仅拆分数据页,还拆分索引页了。

那如果页中有记录被删除呢,怎么去平衡,这时候就有一个东西叫做 计算因子,如果删除后的页的记录数量小于 计算因子*总页数 的时候,B+ 树会去做 合并操作。那我就继续用这一组数据来做示例。

删除 606 638 以后,B+ 树就会变成下面这个样子:

所以说,B+ 树在发生了修改以后,为了保证查询效率,会对某个一部分的节点进行整容。

三.InnoDB是怎么用B+树

好,结合上一篇的 InnoDB表结构 中的页和数据行的结构,以及刚刚所说的 B+树,现在我们就可以来看看一个表中的数据,是怎样被存储以及怎样被查询的。

从之前我们知道,数据是按照数据页的方式进行存放的,而数据页里面除了记录我们的用户记录行意外,还有一些额外的属性用来表示这一页的类型(所以数据页不仅仅是存放数据的页,也可能是 undo页 等等),而数据行里面也有一些额外的属性来辅助查找。

3.1 查找数据所在的页

之前已经系统的列举过 数据页数据行 的所有属性,现在我就挑在索引中需要用到的属性放在图中就可以了,放上其他的没什么用处,也占用地方。

那我现在就假设,上一节中的叶子节点中的数据(如下图),就是我们一条记录的主键 id,因为我们的记录肯定不止有主键这么简单,所以应该还有其他数据,我暂时使用一些占位的方框做代替。

那么这时候上面在数据在 MySQL 数据页中的结构,应该是这样子的:

叶子节点之间的关系,记录类型中:2 表示最小记录,3 表示最大记录,0 表示用户记录。那如果这棵树没有非叶子节点的话,搜索就是线性的需要一个一个遍历,这时候怎么办咧,B+树 的结构就出现了。

这个结构,就是上面 B+树InnoDB 中数据页的结构了。

3.2 查找真实数据

那么,上面我演示的一个页有三个记录,但其实不止三个记录,我就假设一个页有 16 条记录(其实不止,根据数据的大小)吧。那通过索引只能找到这一页呀,难道要一个一个遍历查询相对应的数据,这时候使用遍历还是有点早了(毕竟遍历并没有那么高效),所以在每个数据页中,又使用了一个槽进行分区。

那现在把镜头切换到一个页中来:

InnoDB 会把一个页中的所有数据根据一个很小的基数,比如 4 个记录为一组,去划分分组。那么每个组在数据页中都有对应的槽来记录分组的最大值。

那按照上面的数据,比如我要查询 55

槽0 = 18,槽1 = 54,槽2 = 64,槽3 = 77。

怎么找呢,类似于上面那个二分查找的游戏,槽018,老大说 18-77 之间,槽154,老大说 54-77,直到最后,确定数字是在 槽1槽2 之间,因为 槽2 = 64 大于 55,而 槽1 = 54 小于 55。所以从 槽1 的最大值 54 开始寻找下一条记录,遍历分组里面的数据行(因为这时候需要遍历的长度已经被切到很小了,所以是时候用遍历来做了,因为用其他算法的次数跟遍历差不多),取出 55 这条数据。

那整个查询过程中,需要跟硬盘交互的并不多,我们知道,InnoDB 是通过数据页来进行硬盘与内存的沟通的,查找效率高就意味着,我需要从硬盘取数据的次数越少。

3.3 删除记录

删除记录时,InnoDB 是怎么做的,就是在之前提到的数据行的头信息的 delete_flag 设置为 1,这条记录就会被放入 垃圾链表,当又需要插入新数据的时候,会判断是否合适放在这个位置,再把原来的数据给覆盖掉。

为什么这么做无非就是减少 硬盘IO。如果真的删除的话,需要去同步硬盘,这个过程并不是很重要,所以先标记就好了。

然后这条记录的上一条记录的 next_record 字段将被重新指向当前删除记录的下一条记录。

四.聚簇索引

那聚簇索引是什么,其实看了上面的图,加点料就可以了:

其实上面的例子我使用的是 id 来做例子,但是每条记录还带有其他所有列的信息(非 NULL)的,如果有 NULL 则会被记录在记录头信息,然后节省了存储数据的空间。

而这种索引,叫做 聚簇索引,意思是什么咧,就是一条记录保存了完整的数据信息,非聚簇则不是。而每条数据都会有一个 primary key,如果没有,InnoDB 会自动加上这个隐藏的列。这也是 MySQL_InnoDB 结构的库保存数据的方式。

所以可以这么说,InnoDB 就是 B+树 构成的。而根节点从建立表开始将会被记录,之后每次查询都会从这个根节点开始查询。

五.非聚簇索引

5.1 简单非聚簇索引

非聚簇索引聚簇索引 有什么区别,区别在于 非聚簇索引 并没有存放整条记录的所有数据,只是存放了 索引列主键 而已。所以当我们通过 非聚簇索引 查询一条数据行的所有列的时候,就需要 回表 去查询其他列的信息了,也就是说需要 两次查询 才完成这个需求。

比方一个表:

1
2
3
4
5
6
CREATE TABLE trbac_user (
id BIGINT auto_increase,
name varchar(50) NOT NULL,
mobile varchar(50) NOT NULL
PRIMARY KEY(id)
)

那我使用 name 来做非聚簇索引,这时候 InnoDB 就有一颗以 name 来查询的 B+树

那我们可以看到,数据行并没有覆盖所有数据,只有 name + id。那比如我们只是查询 name 或者 id 列,就可以直接在这里返回去了。

但是如果我还想要知道 mobile 列的话,这时候就需要在上面那棵树拿到 id,然后再去 id 那棵树查询其他的列信息。

需要从下面的主键的数据页来查询其他数据。

那这里有个什么经验呢…就是我们可以根据经常使用常用的列来建立索引,先拿到 id,然后再去缓存命中,再走数据库~

单列聚簇索引还有个需要注意的点,就是这个列的辨识度要搞,如果这个列只有 MANWOMAN 的话,为这个列建立索引根本就没有什么用处,因为跟全表扫描一样的效率,优化器更偏向于全表扫描,所以切记不要给列值只有寥寥几个的列建立索引,纯属浪费。

5.2 多列非聚簇索引

如果是 name + mobile 联合索引的话,需要注意的点就有点多了:

每个数据页的排序就会变成,先按照 name 进行排序,然后再按照 mobile 进行排序。

5.4 什么时候不会使用索引

1.不满足最左匹配

我想大家都听过 最左匹配原则,就是这个意思,因为我的索引是先根据 name 排序再根据 mobile 进行排序的。那么查询的时候,如果加上 order by 的话,这个排序同样也是派上用场的。

但是如果说查询条件只是写了 WHERE mobile = '13800x1',那上面的索引就完全派不上用场。

怎么说,因为我这个索引是结合了 name 先做排序然后索引的,你只是查询 mobile 的话,就需要遍历整个索引,拿到 id 后还需要回表,这个过程跟直接扫描 聚簇索引 相比,耗费的资源更多,所以,如果一个 name + mobile 的索引,只应用于带有 name 开始的查询条件。因为 name 在前面。这就是 最左匹配原则

2.排序一个列使用升序一个使用降序

比如 WHERE name like 'xxx%' ORDER BY name DESC,mobile ASC,这样的情况只能用到 name 列的索引(也就是所有 mobile 失去了效果),然后再把所有列重新整理,依据 mobile 重新排序。

如果两个都是同一个方向的排序就不会出现这种情况。

3.VARCHAR 匹配前缀查询

WHERE name like '%狗蛋' 这种情况为什么不能使用索引呢,因为你前面未知啊,根据 name 排序的索引查询跟全表扫描有什么区别,还少了 回表 的操作,所以只能全表扫描,取出满足 xxx狗蛋 的数据了。

4.排序用到索引中没有的列

这个理解了 区别在于 非聚簇索引 并没有存放整条记录的所有数据,只是存放了 索引列主键 而已 这句话,就知道为什么了,因为索引中的排序并不适合 SQL 指定的顺序。

5.使用函数修饰值的时候

比如 WHERE LOWER(name) = 'aa',每个列都需要通过函数调用,那就跟全表扫描没什么区别了。


那如果 SQL 语句中,条件出现的顺序没有和多列索引的顺序一致的话,会怎么样:

完全不必担心这个问题,因为 SQL 在执行之前还会交给一个叫做 优化器 的东西进行整理,下面会说到,现在就只要知道他会把 SQL 条件整理成跟索引一样的顺序就好了。

既然多列索引可以帮忙查询条件中满足最左匹配的 SQL 语句,是不是越多越好,答案并不是,我觉得应该根据权重来区分,因为 非聚簇索引 他的索引列并不会像自增 ID 的索引树一样,顺序的插入数据(历史原因导致我们项目使用 UUID 做主键的哭晕在厕所),而是经常的造成 页拆分(也就是上面拆分的动图),所以 非聚簇索引 多了,增删改 需要维护这棵树平衡所做的 整容 就越多,严重的话会影响 增删改 的效率。


5.5 什么时候命中索引

那什么时候用到了索引,自然就是避免上面的情况就可以了,就有这些情况,包括但不局限于:

  1. 条件覆盖了索引的最左列;
  2. 常量查询,WHERE name = 'xxx'
  3. 范围查询,满足 1 的情况下,比如 name > 'AA' 或者 name = 'AA' AND mobile > '13800';
  4. 排序,但是需要用到的排序列是同一个方向进行排序的。
  5. 分组,依然需要满足 1 条件,如果用到了 group by nameInnoDB 会先将相同的 name 放在一起,然后再继续根据其他条件进行运算。

5.6 回表代价

可以这么说,如果回表的代价过高,InnoDB解析器 可能直接决定,不要使用索引,直接全表扫描。

怎么回事,就是 非聚簇 索引他的值都是连在一起的,查询的时候,称为 顺序I/O,而 非聚簇索引 查询出来的 id 并不会连在一起,所以回表去查询 id 数据的时候,称为 随机I/O。而 随机I/O 的效率是很低的,所以当命中索引的数据行总是较多的时候,不如 全表扫描 来的快,所以可能出现 SQL 已经完全命中这个索引但是解析器他就是不使用。

怎么防止这种情况发生,就是让查询索引返回的数量少,比如我们可以 LIMIT 100 去限定只查询 100 条数据,或者我们只要查询索引中出现的列,比如 id 然后再在程序中,根据 id 列表去命中缓存,名不中的再一次性批量查库就可以了。

5.7 文件排序

有时候我们去解析 EXPLAIN 我们的 SQL 语句的时候,会出现 File Sort 类型,这个类型指的是,查询出来的结果不是按照我们 SQL 指定的顺序来的,所以需要在内存中(必要时借助硬盘)来重新对数据进行排序,这个过程就是 File Sort,那如何避免这个类型出现,那就是防止上面不命中索引的情况。

5.8 索引合并

使用等值且命中两个索引的列

比方说我的用户表有 name 索引 和 mobile 索引,那这个条件 WHERE name = 'Weidan' AND mobile = '18588777777' 就可以使用两个索引,然后取 id交集,然后回表查询。但是如果 name > 'Weidan' 这种条件,就不会使用到合并。

但是但是,如果是主键列是范围的话,是可以的,因为他们合并的时候也会操作主键嘛。

不过有没有使用到,还要看成本,如果一个索引命中的数据太多,依然不会使用合并索引。

合并两个索引的列

依然需要满足 列都是等值查询 这个条件,比如 WHERE name = 'Weidan' OR mobile = '18588777777',会合并两个主键再统一回表查询。

范围搜索合并查询

不过如果我们是范围查询,又想使用两个索引怎么办,我们可以把 name > 'Weidan' 以及 monile > '18588777777' 先查询出来 id 然后 union 在一起,再通过 id 去寻找我们想要的数据。

但是还是建议把需要合并的列作为一个 非聚簇 索引来覆盖,效率要高。

七.表连接查询

首先就以最经典的学生、成绩来做示例吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
CREATE TABLE `student` (
`id` int(11) NOT NULL,
`name` varchar(50) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

BEGIN;
INSERT INTO `student` VALUES (1, '狗蛋');
INSERT INTO `student` VALUES (2, '狗剩');
INSERT INTO `student` VALUES (3, '翠花');
COMMIT;

DROP TABLE IF EXISTS `scope`;
CREATE TABLE `scope` (
`id` int(11) NOT NULL,
`subject` varchar(255) DEFAULT NULL,
`scope` int(255) DEFAULT NULL,
`stu_id` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

BEGIN;
INSERT INTO `scope` VALUES (1, '语文', 80, 1);
INSERT INTO `scope` VALUES (2, '数学', 98, 1);
INSERT INTO `scope` VALUES (3, '英语', 59, 2);
INSERT INTO `scope` VALUES (4, '政治', 79, 2);
COMMIT;

7.1 连表查询

那么先列举一个连表很普通的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
SELECT * 
FROM student, scope;

+----+--------+----+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+----+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 2 | 狗剩 | 1 | 语文 | 80 | 1 |
| 3 | 翠花 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 2 | 数学 | 98 | 1 |
| 3 | 翠花 | 2 | 数学 | 98 | 1 |
| 1 | 狗蛋 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 3 | 翠花 | 3 | 英语 | 59 | 2 |
| 1 | 狗蛋 | 4 | 政治 | 79 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
| 3 | 翠花 | 4 | 政治 | 79 | 2 |
+----+--------+----+---------+-------+--------+
12 rows in set (0.00 sec)

可以看到,如果我们不加任何条件,直接就连接两张表的话,取出来的结果就是第一张表所有的数据,去搭配第二张表所有的数据,也就是第一张表有 3 条数据,第二张表有 4 条记录,结果就有 3 ✖ 4 = 12 条记录。这个过程也称为 笛卡尔积。这种情况在日常编程中应该也没人会这么写的吧,仔细想想,如果两张表都是百万级别的数据,笛卡尔积是多恐怖的一件事情。

7.2 内连接

就是由于上面的结果集一般我们都不需要,所以我们会加上一些条件加以限制:

1
2
3
4
5
6
7
8
9
10
11
12
13
SELECT stu.*, scp.*
FROM student stu, scope scp
WHERE stu.id = scp.stu_id;

+----+--------+----+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+----+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
+----+--------+----+---------+-------+--------+
4 rows in set (0.00 sec)

这就是我们想要的结果集,所有参考学生的考试成绩。

InnoDB 是怎样的实现两个表连接的呢,这时候,优化器会根据查询成本确定 驱动表被驱动表

  • 驱动表:先查询的那个表,这里假设是 student 表,所以执行的时候,第一步执行 SELECT * FROM student;

  • 被驱动表:查询出来驱动表所有的数据以后,就会根据驱动表中每一条数据,去执行被驱动表。

    • 比如我现在 student 有三条记录,分别 id1 2 3
    • 然后对三条记录分别执行 SELECT * FROM scope WHERE stu_id = ?
    • 因为 scope 表中不存在 翠花 的成绩,所以 SELECT * FROM scope WHERE stu_id = 3 并没有展示出来,因为这个语句相当于 SELECT * FROM scope WHERE stu_id NOT NULL AND stu_id = 3

那如果上面的语句加上其他条件会怎么样,比如:

1
2
3
4
5
# 查询成绩 90 分以上的学生成绩信息
SELECT stu.*, scp.*
FROM student stu, scope scp
WHERE stu.id = scp.stu_id
AND scp.scope > 90;

那在访问驱动表 student 的时候同上面一样,但是针对驱动表中每一条数据执行被驱动表的时候,就会变成:

SELECT * FROM scope WHERE stu_id NOT NULL AND stu_id = ? AND scope > 90;

一句话就是说,这些条件针对哪个表,在执行那个表的时候就会带上指定的条件。

内连接推荐写法:

1
2
3
SELECT stu.*, scp.*
FROM student stu INNER JOIN scope scp ON stu.id = scp.stu_id
WHERE scp.scope > 90;

效率没有变高,但是可读性变好,在我看来,特定的限定功能就需要放在特定的位置,连接条件在 ON 后面,而查询条件在 WHERE 后面,也可以很好的跟下面的外连接写法统一。

7.3 外连接

外连接的目的是将连接在一起查询的表中,以某个表为主要目的,查询他在另外一个表的所有信息,包括不存在的信息。比如还是上面的例子,翠花她是个坏学生,一个科目都没有参加考试,但是老师如果通过上面的内连接查询来看的话,可能都不知道还有这个人的存在。这就麻烦大了啊,家长上门投诉,我给你钱了,她没参加考试你都不管管的吗。所以这时候,就需要外连接来做这个事情了:

1
2
3
4
5
6
7
8
9
10
11
12
SELECT * 
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id;
+----+--------+------+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+------+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
| 3 | 翠花 | NULL | NULL | NULL | NULL |
+----+--------+------+---------+-------+--------+
5 rows in set (0.00 sec)

这个时候,嗯哼,一抓一个准,没有来考试的,在 scope 表的字段中,显示为 NULL

关于 驱动表被驱动表 的含义已经在上面内连接的内容中指定,那么如果说 内连接 是根据成本来指定 驱动表 的,那 外连接 就是我们来指定驱动表。

驱动表 所有满足我们条件的数据,来查询 被驱动表,如果 被驱动表 不存在 驱动表 中某条数据的关联,显示为 NULL

那么关键点在哪里,就在这个 ON 后面的条件,ON 后面的条件,是指定 被驱动表 中不满足 ON 条件情况下依然要显示的关键。(在内连接中连接条件放 ON 和放 WHERE 效果一样)

如果外连接有几层,比如说三层:

1
2
3
4
SELECT *
FROM teacher t
LEFT JOIN student ON t.id = stu.into
LEFT JOIN scope scp ON stu.id = scope.stu_id;

那么上面这条查询需要怎么走呢,先查询老师所有的学生,得到一张中间表以后,再以这张中间表作为 驱动表 来查询 scope

八.复杂条件查询

MySQL InnoDB 中存在着 SQL解析器,解析器可谓为了查询正确的数据而费尽心思,目的只有一个,就是较快的查询出来正确的数据。其实这里也可以联想到 jvm编译器,为了执行效率比较快,会对字节码进行一些顺序的重新编排,什么 happen before 规则都出来了。

而什么东西查询最快呢,那就是查询常量的时候了。

8.1 整理查询语句

首先就是去除不必要的括号,因为括号如果没有意义存在,那移除了就更有利于优化器的解析了。

比如 WHERE (id = 1) 会被优化成 WHERE id = 1。这个好像也没什么好说的。

复杂一点的 WHERE (id = 1 AND name = 'WEIDAN') AND age = 20 则会被优化成 WHERE id = 1 AND name = 'WEIDAN' AND age = 20

8.2 简化查询条件

WHERE age > 18 AND real_age > age 则直接变成 WHERE age > 18 AND real_age > 18

8.3 删除废话条件

像我之前老师教的,在写 MyBatis Mapper.xml 的时候,因为 WHERE 总是需要动态的条件,所以前面会加一个 WHERE 1 = 1(为啥不用标签,因为标签可读性其实不怎么高)。

那么,如果我写的是 WHERE 1 = 1 AND name LIKE 'Weidan%',那么语句将会被直接优化为 WHERE name LIKE 'Weidan%' 。这么想想好像直接写 WHERE 1 = 1 除了解析器多了点工作,好像也没什么所谓了。

8.4 计算表达式

WHERE age = 9 + 9 那么解析器会直接给你个 WHERE age = 18。不过如果这些计算方式放在了列名上,比如:

1
2
WHERE -age = -18
WHERE to_days(age) = 18

解析器害怕他也优化错了,所以也会放弃优化,一个简单的记忆就是函数发生在等号后面的常量值上,可以优化,但是如果需要依赖列的计算的话,那就放弃优化了。

8.5 必要时转换外连接到内连接

右外连接会被转换为左外连接,毕竟 左外连接 更好确定 驱动表被驱动表

那如果我们在查询 左外连接 的时候,暗示着我得出的结果中,被驱动表不含 NULL 的值,聪明的暖男解析器将会 GET 到这个信息,把 外连接 优化成 内连接

比如上面的例子中:

1
2
3
4
5
6
7
SELECT * 
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id
AND scp.id = 1;

SELECT *
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id;
AND scp.id IS NOT NULL;

那么上面两个语句中是不是就都暗示解析器中,拿到的 scope 中的 id 不能为空。

所以!直接变成这条语句执行:

1
2
3
4
5
6
SELECT * 
FROM student stu INNER JOIN scope scp ON scp.stu_id = stu.id
AND scp.id = 1;

SELECT *
FROM student stu INNER JOIN scope scp ON scp.stu_id = stu.id;;

九.完

所以说,B+ 树在发生了修改以后,为了保证查询效率,会对某个一部分的节点进行整容。

三.InnoDB是怎么用B+树

好,结合上一篇的 InnoDB表结构 中的页和数据行的结构,以及刚刚所说的 B+树,现在我们就可以来看看一个表中的数据,是怎样被存储以及怎样被查询的。

从之前我们知道,数据是按照数据页的方式进行存放的,而数据页里面除了记录我们的用户记录行意外,还有一些额外的属性用来表示这一页的类型(所以数据页不仅仅是存放数据的页,也可能是 undo页 等等),而数据行里面也有一些额外的属性来辅助查找。

3.1 查找数据所在的页

之前已经系统的列举过 数据页数据行 的所有属性,现在我就挑在索引中需要用到的属性放在图中就可以了,放上其他的没什么用处,也占用地方。

那我现在就假设,上一节中的叶子节点中的数据(如下图),就是我们一条记录的主键 id,因为我们的记录肯定不止有主键这么简单,所以应该还有其他数据,我暂时使用一些占位的方框做代替。

一.InnoDB表数据

上面聊了这么多这个结构,那个结构的。现在是不是有点好奇,InnoDB 是把数据存在哪里的。答案也很简单,存在一颗 B+ 树种。

InnoDB 中,数据所在的位置没有其他地方,就只有一颗 B+ 树。而我们自己建立的索引,也是一颗 B+ 树。存储完整数据的 B+ 树也叫 聚簇索引,而我们自定义的列索引的 B+ 树,则称为 非聚簇索引

OK,大概明白了这两个东西以后,我们一个一个拆开来说说。

二.B+树是什么

关乎数据结构的东西了,如果觉得很简单,或者说已经学过脑袋有印象,可以跳过这段一个非计算机老男人写的废话。

B+ 树,一切要从一个最简单的例子开始,就是二分查找法。

2.1 二分查找法

我们在聚合的时候,估计都玩过一个游戏,叫做大冒险。确定谁来大冒险,有个方式就是猜数字:由上一轮中的倒霉鬼,来确定一个数字,然后围成一圈的小伙伴们来猜。猜中了,就是下一个倒霉鬼。

那怎么猜的呢,这个倒霉鬼在手机上输入一个数字,盖在地上防止修改。然后从一圈中某个人开始,说一个数字,那么那个站在台上的倒霉蛋就会缩小范围,让下一个人继续猜:

比如数字是69。

那么就有下面的对话:

1
2
3
4
5
6
A: 50
倒霉:50 - 100
B:75
倒霉:50 - 75
C:69
倒霉:恭喜你答对了balabala

这个过程咧,有没有很熟悉,对,就是 二分查找法


那为啥使用二分查找法呢,肯定是因为性能好啊。

那比如我有个 有序 数组:[18, 53, 55, 147, 151, 495, 551, 606, 638, 728]

我一次查询每一个数字,平均次数是 (1+2+3+4+5+6+7+8+9+10)/10 = 5.5次,而使用二分查找法则是 4+3+2+4+3+1+4+3+2+3)/10 = 2.9次。顺序查找最坏的情况也要 10 次,而二分查找最坏是 4 次。这么好的查找效率没有理由不用他。

2.2 二叉查找树

那二分查找和树什么关系,这里就要说到 二叉查找树,根据百度百科的定义:

一棵空树,或者是具有下列性质的二叉树

(1)若左子树不空,则左子树上所有结点的值均小于它的根结点的值;

(2)若右子树不空,则右子树上所有结点的值均大于它的根结点的值;

(3)左、右子树也分别为二叉排序树;

(4)没有键值相等的结点。

那么我现在把上面的数组转换成 二叉查找树,并且给予一个查找的动图:

查询看起来是很方便,但是对数据的增删查改就不是这样的了。有时候,数据插入多了,如果不调整可能会所有都放在了左边或者右边,这样性能就会越来越接近顺序查找。那么,数学家就提出了 平衡二叉树最优二叉树 的定义。

2.3 平衡二叉树

平衡二叉树:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1。这样子就可以让树变得矮胖矮胖的好身材,至于为啥矮胖矮胖才是好身材,因为这样子的话,查询效率才高啊。

比如下面这颗,查找大于 495 的时候,简直就是线性查找的效率了。

image-20200113155115465

那长得丑怎么办,整容(旋转跳跃我闭着眼)啊,不过需要钱(性能)。

那我现在就按照 二叉查找树 那一颗,来插入一个 999,平衡二叉树是在增删改的时候通过旋转来平衡树。

那么有长得帅的读者就要问了,这个跟 B+树 是什么关系。

2.4 B+树

因为 B+树 跟上面的思想差不多,所以,现在就来说说 B+树

B+树 她不是二叉树,但是查询跟 二叉树 差不多,我先放一颗 B+树

这是一颗高度为 2 扇出为 5B+树

那这个怎么实现查找的呢,现在比方说我要查找 606 这条数据:

那么 B+树 是怎么实现新增,然后平衡的呢,跟上面一样发生旋转。旋转的规则如下:

叶子节点是第二层的数据,索引节点是第一层查找时用到的数据。那旋转的情况就是节点页有没有满了:

叶子节点 索引节点 操作
× × 直接插入
× 1. 拆分叶子节点;
2. 中间节点值存入索引节点;
3. 小于中间节点的数据放左叶子,大于或等于放入右叶子。
1. 按照上面情况拆分叶子节点;
2. 然后根据相同的操作再拆分索引节点

第一种就不用演示了,比如新增 1000 直接存入最后边的叶子节点就好了。

接下来我演示第二种,那我就插入 645 这个数字吧。

可以从图中看到,拆分页是多么耗性能的一个东西了,所以我们经常说顺序插入是最好的性能,也要求主键一般呈递增的状态。

但是当左右兄弟节点可能没有满的时候,InnoDB不会急着去拆分数据页,而是会通过旋转的方式来让树达到平衡。这里就不说了。

那如果是第三种情况呢,拆分索引页跟拆分数据页是同样的道理,我就不画出来了,只要知道如果是第三种,性能还要比第二种低一倍,因为不仅拆分数据页,还拆分索引页了。

那如果页中有记录被删除呢,怎么去平衡,这时候就有一个东西叫做 计算因子,如果删除后的页的记录数量小于 计算因子*总页数 的时候,B+ 树会去做 合并操作。那我就继续用这一组数据来做示例。

删除 606 638 以后,B+ 树就会变成下面这个样子:

所以说,B+ 树在发生了修改以后,为了保证查询效率,会对某个一部分的节点进行整容。

三.InnoDB是怎么用B+树

好,结合上一篇的 InnoDB表结构 中的页和数据行的结构,以及刚刚所说的 B+树,现在我们就可以来看看一个表中的数据,是怎样被存储以及怎样被查询的。

从之前我们知道,数据是按照数据页的方式进行存放的,而数据页里面除了记录我们的用户记录行意外,还有一些额外的属性用来表示这一页的类型(所以数据页不仅仅是存放数据的页,也可能是 undo页 等等),而数据行里面也有一些额外的属性来辅助查找。

3.1 查找数据所在的页

之前已经系统的列举过 数据页数据行 的所有属性,现在我就挑在索引中需要用到的属性放在图中就可以了,放上其他的没什么用处,也占用地方。

那我现在就假设,上一节中的叶子节点中的数据(如下图),就是我们一条记录的主键 id,因为我们的记录肯定不止有主键这么简单,所以应该还有其他数据,我暂时使用一些占位的方框做代替。

那么这时候上面在数据在 MySQL 数据页中的结构,应该是这样子的:

叶子节点之间的关系,记录类型中:2 表示最小记录,3 表示最大记录,0 表示用户记录。那如果这棵树没有非叶子节点的话,搜索就是线性的需要一个一个遍历,这时候怎么办咧,B+树 的结构就出现了。

这个结构,就是上面 B+树InnoDB 中数据页的结构了。

3.2 查找真实数据

那么,上面我演示的一个页有三个记录,但其实不止三个记录,我就假设一个页有 16 条记录(其实不止,根据数据的大小)吧。那通过索引只能找到这一页呀,难道要一个一个遍历查询相对应的数据,这时候使用遍历还是有点早了(毕竟遍历并没有那么高效),所以在每个数据页中,又使用了一个槽进行分区。

那现在把镜头切换到一个页中来:

InnoDB 会把一个页中的所有数据根据一个很小的基数,比如 4 个记录为一组,去划分分组。那么每个组在数据页中都有对应的槽来记录分组的最大值。

那按照上面的数据,比如我要查询 55

槽0 = 18,槽1 = 54,槽2 = 64,槽3 = 77。

怎么找呢,类似于上面那个二分查找的游戏,槽018,老大说 18-77 之间,槽154,老大说 54-77,直到最后,确定数字是在 槽1槽2 之间,因为 槽2 = 64 大于 55,而 槽1 = 54 小于 55。所以从 槽1 的最大值 54 开始寻找下一条记录,遍历分组里面的数据行(因为这时候需要遍历的长度已经被切到很小了,所以是时候用遍历来做了,因为用其他算法的次数跟遍历差不多),取出 55 这条数据。

那整个查询过程中,需要跟硬盘交互的并不多,我们知道,InnoDB 是通过数据页来进行硬盘与内存的沟通的,查找效率高就意味着,我需要从硬盘取数据的次数越少。

3.3 删除记录

删除记录时,InnoDB 是怎么做的,就是在之前提到的数据行的头信息的 delete_flag 设置为 1,这条记录就会被放入 垃圾链表,当又需要插入新数据的时候,会判断是否合适放在这个位置,再把原来的数据给覆盖掉。

为什么这么做无非就是减少 硬盘IO。如果真的删除的话,需要去同步硬盘,这个过程并不是很重要,所以先标记就好了。

然后这条记录的上一条记录的 next_record 字段将被重新指向当前删除记录的下一条记录。

四.聚簇索引

那聚簇索引是什么,其实看了上面的图,加点料就可以了:

其实上面的例子我使用的是 id 来做例子,但是每条记录还带有其他所有列的信息(非 NULL)的,如果有 NULL 则会被记录在记录头信息,然后节省了存储数据的空间。

而这种索引,叫做 聚簇索引,意思是什么咧,就是一条记录保存了完整的数据信息,非聚簇则不是。而每条数据都会有一个 primary key,如果没有,InnoDB 会自动加上这个隐藏的列。这也是 MySQL_InnoDB 结构的库保存数据的方式。

所以可以这么说,InnoDB 就是 B+树 构成的。而根节点从建立表开始将会被记录,之后每次查询都会从这个根节点开始查询。

五.非聚簇索引

5.1 简单非聚簇索引

非聚簇索引聚簇索引 有什么区别,区别在于 非聚簇索引 并没有存放整条记录的所有数据,只是存放了 索引列主键 而已。所以当我们通过 非聚簇索引 查询一条数据行的所有列的时候,就需要 回表 去查询其他列的信息了,也就是说需要 两次查询 才完成这个需求。

比方一个表:

1
2
3
4
5
6
CREATE TABLE trbac_user (
id BIGINT auto_increase,
name varchar(50) NOT NULL,
mobile varchar(50) NOT NULL
PRIMARY KEY(id)
)

那我使用 name 来做非聚簇索引,这时候 InnoDB 就有一颗以 name 来查询的 B+树

那我们可以看到,数据行并没有覆盖所有数据,只有 name + id。那比如我们只是查询 name 或者 id 列,就可以直接在这里返回去了。

但是如果我还想要知道 mobile 列的话,这时候就需要在上面那棵树拿到 id,然后再去 id 那棵树查询其他的列信息。

需要从下面的主键的数据页来查询其他数据。

那这里有个什么经验呢…就是我们可以根据经常使用常用的列来建立索引,先拿到 id,然后再去缓存命中,再走数据库~

单列聚簇索引还有个需要注意的点,就是这个列的辨识度要搞,如果这个列只有 MANWOMAN 的话,为这个列建立索引根本就没有什么用处,因为跟全表扫描一样的效率,优化器更偏向于全表扫描,所以切记不要给列值只有寥寥几个的列建立索引,纯属浪费。

5.2 多列非聚簇索引

如果是 name + mobile 联合索引的话,需要注意的点就有点多了:

每个数据页的排序就会变成,先按照 name 进行排序,然后再按照 mobile 进行排序。

5.4 什么时候不会使用索引

1.不满足最左匹配

我想大家都听过 最左匹配原则,就是这个意思,因为我的索引是先根据 name 排序再根据 mobile 进行排序的。那么查询的时候,如果加上 order by 的话,这个排序同样也是派上用场的。

但是如果说查询条件只是写了 WHERE mobile = '13800x1',那上面的索引就完全派不上用场。

怎么说,因为我这个索引是结合了 name 先做排序然后索引的,你只是查询 mobile 的话,就需要遍历整个索引,拿到 id 后还需要回表,这个过程跟直接扫描 聚簇索引 相比,耗费的资源更多,所以,如果一个 name + mobile 的索引,只应用于带有 name 开始的查询条件。因为 name 在前面。这就是 最左匹配原则

2.排序一个列使用升序一个使用降序

比如 WHERE name like 'xxx%' ORDER BY name DESC,mobile ASC,这样的情况只能用到 name 列的索引(也就是所有 mobile 失去了效果),然后再把所有列重新整理,依据 mobile 重新排序。

如果两个都是同一个方向的排序就不会出现这种情况。

3.VARCHAR 匹配前缀查询

WHERE name like '%狗蛋' 这种情况为什么不能使用索引呢,因为你前面未知啊,根据 name 排序的索引查询跟全表扫描有什么区别,还少了 回表 的操作,所以只能全表扫描,取出满足 xxx狗蛋 的数据了。

4.排序用到索引中没有的列

这个理解了 区别在于 非聚簇索引 并没有存放整条记录的所有数据,只是存放了 索引列主键 而已 这句话,就知道为什么了,因为索引中的排序并不适合 SQL 指定的顺序。

5.使用函数修饰值的时候

比如 WHERE LOWER(name) = 'aa',每个列都需要通过函数调用,那就跟全表扫描没什么区别了。


那如果 SQL 语句中,条件出现的顺序没有和多列索引的顺序一致的话,会怎么样:

完全不必担心这个问题,因为 SQL 在执行之前还会交给一个叫做 优化器 的东西进行整理,下面会说到,现在就只要知道他会把 SQL 条件整理成跟索引一样的顺序就好了。

既然多列索引可以帮忙查询条件中满足最左匹配的 SQL 语句,是不是越多越好,答案并不是,我觉得应该根据权重来区分,因为 非聚簇索引 他的索引列并不会像自增 ID 的索引树一样,顺序的插入数据(历史原因导致我们项目使用 UUID 做主键的哭晕在厕所),而是经常的造成 页拆分(也就是上面拆分的动图),所以 非聚簇索引 多了,增删改 需要维护这棵树平衡所做的 整容 就越多,严重的话会影响 增删改 的效率。


5.5 什么时候命中索引

那什么时候用到了索引,自然就是避免上面的情况就可以了,就有这些情况,包括但不局限于:

  1. 条件覆盖了索引的最左列;
  2. 常量查询,WHERE name = 'xxx'
  3. 范围查询,满足 1 的情况下,比如 name > 'AA' 或者 name = 'AA' AND mobile > '13800';
  4. 排序,但是需要用到的排序列是同一个方向进行排序的。
  5. 分组,依然需要满足 1 条件,如果用到了 group by nameInnoDB 会先将相同的 name 放在一起,然后再继续根据其他条件进行运算。

5.6 回表代价

可以这么说,如果回表的代价过高,InnoDB解析器 可能直接决定,不要使用索引,直接全表扫描。

怎么回事,就是 非聚簇 索引他的值都是连在一起的,查询的时候,称为 顺序I/O,而 非聚簇索引 查询出来的 id 并不会连在一起,所以回表去查询 id 数据的时候,称为 随机I/O。而 随机I/O 的效率是很低的,所以当命中索引的数据行总是较多的时候,不如 全表扫描 来的快,所以可能出现 SQL 已经完全命中这个索引但是解析器他就是不使用。

怎么防止这种情况发生,就是让查询索引返回的数量少,比如我们可以 LIMIT 100 去限定只查询 100 条数据,或者我们只要查询索引中出现的列,比如 id 然后再在程序中,根据 id 列表去命中缓存,名不中的再一次性批量查库就可以了。

5.7 文件排序

有时候我们去解析 EXPLAIN 我们的 SQL 语句的时候,会出现 File Sort 类型,这个类型指的是,查询出来的结果不是按照我们 SQL 指定的顺序来的,所以需要在内存中(必要时借助硬盘)来重新对数据进行排序,这个过程就是 File Sort,那如何避免这个类型出现,那就是防止上面不命中索引的情况。

5.8 索引合并

使用等值且命中两个索引的列

比方说我的用户表有 name 索引 和 mobile 索引,那这个条件 WHERE name = 'Weidan' AND mobile = '18588777777' 就可以使用两个索引,然后取 id交集,然后回表查询。但是如果 name > 'Weidan' 这种条件,就不会使用到合并。

但是但是,如果是主键列是范围的话,是可以的,因为他们合并的时候也会操作主键嘛。

不过有没有使用到,还要看成本,如果一个索引命中的数据太多,依然不会使用合并索引。

合并两个索引的列

依然需要满足 列都是等值查询 这个条件,比如 WHERE name = 'Weidan' OR mobile = '18588777777',会合并两个主键再统一回表查询。

范围搜索合并查询

不过如果我们是范围查询,又想使用两个索引怎么办,我们可以把 name > 'Weidan' 以及 monile > '18588777777' 先查询出来 id 然后 union 在一起,再通过 id 去寻找我们想要的数据。

但是还是建议把需要合并的列作为一个 非聚簇 索引来覆盖,效率要高。

七.表连接查询

首先就以最经典的学生、成绩来做示例吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
CREATE TABLE `student` (
`id` int(11) NOT NULL,
`name` varchar(50) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

BEGIN;
INSERT INTO `student` VALUES (1, '狗蛋');
INSERT INTO `student` VALUES (2, '狗剩');
INSERT INTO `student` VALUES (3, '翠花');
COMMIT;

DROP TABLE IF EXISTS `scope`;
CREATE TABLE `scope` (
`id` int(11) NOT NULL,
`subject` varchar(255) DEFAULT NULL,
`scope` int(255) DEFAULT NULL,
`stu_id` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

BEGIN;
INSERT INTO `scope` VALUES (1, '语文', 80, 1);
INSERT INTO `scope` VALUES (2, '数学', 98, 1);
INSERT INTO `scope` VALUES (3, '英语', 59, 2);
INSERT INTO `scope` VALUES (4, '政治', 79, 2);
COMMIT;

7.1 连表查询

那么先列举一个连表很普通的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
SELECT * 
FROM student, scope;

+----+--------+----+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+----+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 2 | 狗剩 | 1 | 语文 | 80 | 1 |
| 3 | 翠花 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 2 | 数学 | 98 | 1 |
| 3 | 翠花 | 2 | 数学 | 98 | 1 |
| 1 | 狗蛋 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 3 | 翠花 | 3 | 英语 | 59 | 2 |
| 1 | 狗蛋 | 4 | 政治 | 79 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
| 3 | 翠花 | 4 | 政治 | 79 | 2 |
+----+--------+----+---------+-------+--------+
12 rows in set (0.00 sec)

可以看到,如果我们不加任何条件,直接就连接两张表的话,取出来的结果就是第一张表所有的数据,去搭配第二张表所有的数据,也就是第一张表有 3 条数据,第二张表有 4 条记录,结果就有 3 ✖ 4 = 12 条记录。这个过程也称为 笛卡尔积。这种情况在日常编程中应该也没人会这么写的吧,仔细想想,如果两张表都是百万级别的数据,笛卡尔积是多恐怖的一件事情。

7.2 内连接

就是由于上面的结果集一般我们都不需要,所以我们会加上一些条件加以限制:

1
2
3
4
5
6
7
8
9
10
11
12
13
SELECT stu.*, scp.*
FROM student stu, scope scp
WHERE stu.id = scp.stu_id;

+----+--------+----+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+----+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
+----+--------+----+---------+-------+--------+
4 rows in set (0.00 sec)

这就是我们想要的结果集,所有参考学生的考试成绩。

InnoDB 是怎样的实现两个表连接的呢,这时候,优化器会根据查询成本确定 驱动表被驱动表

  • 驱动表:先查询的那个表,这里假设是 student 表,所以执行的时候,第一步执行 SELECT * FROM student;

  • 被驱动表:查询出来驱动表所有的数据以后,就会根据驱动表中每一条数据,去执行被驱动表。

    • 比如我现在 student 有三条记录,分别 id1 2 3
    • 然后对三条记录分别执行 SELECT * FROM scope WHERE stu_id = ?
    • 因为 scope 表中不存在 翠花 的成绩,所以 SELECT * FROM scope WHERE stu_id = 3 并没有展示出来,因为这个语句相当于 SELECT * FROM scope WHERE stu_id NOT NULL AND stu_id = 3

那如果上面的语句加上其他条件会怎么样,比如:

1
2
3
4
5
# 查询成绩 90 分以上的学生成绩信息
SELECT stu.*, scp.*
FROM student stu, scope scp
WHERE stu.id = scp.stu_id
AND scp.scope > 90;

那在访问驱动表 student 的时候同上面一样,但是针对驱动表中每一条数据执行被驱动表的时候,就会变成:

SELECT * FROM scope WHERE stu_id NOT NULL AND stu_id = ? AND scope > 90;

一句话就是说,这些条件针对哪个表,在执行那个表的时候就会带上指定的条件。

内连接推荐写法:

1
2
3
SELECT stu.*, scp.*
FROM student stu INNER JOIN scope scp ON stu.id = scp.stu_id
WHERE scp.scope > 90;

效率没有变高,但是可读性变好,在我看来,特定的限定功能就需要放在特定的位置,连接条件在 ON 后面,而查询条件在 WHERE 后面,也可以很好的跟下面的外连接写法统一。

7.3 外连接

外连接的目的是将连接在一起查询的表中,以某个表为主要目的,查询他在另外一个表的所有信息,包括不存在的信息。比如还是上面的例子,翠花她是个坏学生,一个科目都没有参加考试,但是老师如果通过上面的内连接查询来看的话,可能都不知道还有这个人的存在。这就麻烦大了啊,家长上门投诉,我给你钱了,她没参加考试你都不管管的吗。所以这时候,就需要外连接来做这个事情了:

1
2
3
4
5
6
7
8
9
10
11
12
SELECT * 
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id;
+----+--------+------+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+------+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
| 3 | 翠花 | NULL | NULL | NULL | NULL |
+----+--------+------+---------+-------+--------+
5 rows in set (0.00 sec)

这个时候,嗯哼,一抓一个准,没有来考试的,在 scope 表的字段中,显示为 NULL

关于 驱动表被驱动表 的含义已经在上面内连接的内容中指定,那么如果说 内连接 是根据成本来指定 驱动表 的,那 外连接 就是我们来指定驱动表。

驱动表 所有满足我们条件的数据,来查询 被驱动表,如果 被驱动表 不存在 驱动表 中某条数据的关联,显示为 NULL

那么关键点在哪里,就在这个 ON 后面的条件,ON 后面的条件,是指定 被驱动表 中不满足 ON 条件情况下依然要显示的关键。(在内连接中连接条件放 ON 和放 WHERE 效果一样)

如果外连接有几层,比如说三层:

1
2
3
4
SELECT *
FROM teacher t
LEFT JOIN student ON t.id = stu.into
LEFT JOIN scope scp ON stu.id = scope.stu_id;

那么上面这条查询需要怎么走呢,先查询老师所有的学生,得到一张中间表以后,再以这张中间表作为 驱动表 来查询 scope

八.复杂条件查询

MySQL InnoDB 中存在着 SQL解析器,解析器可谓为了查询正确的数据而费尽心思,目的只有一个,就是较快的查询出来正确的数据。其实这里也可以联想到 jvm编译器,为了执行效率比较快,会对字节码进行一些顺序的重新编排,什么 happen before 规则都出来了。

而什么东西查询最快呢,那就是查询常量的时候了。

8.1 整理查询语句

首先就是去除不必要的括号,因为括号如果没有意义存在,那移除了就更有利于优化器的解析了。

比如 WHERE (id = 1) 会被优化成 WHERE id = 1。这个好像也没什么好说的。

复杂一点的 WHERE (id = 1 AND name = 'WEIDAN') AND age = 20 则会被优化成 WHERE id = 1 AND name = 'WEIDAN' AND age = 20

8.2 简化查询条件

WHERE age > 18 AND real_age > age 则直接变成 WHERE age > 18 AND real_age > 18

8.3 删除废话条件

像我之前老师教的,在写 MyBatis Mapper.xml 的时候,因为 WHERE 总是需要动态的条件,所以前面会加一个 WHERE 1 = 1(为啥不用标签,因为标签可读性其实不怎么高)。

那么,如果我写的是 WHERE 1 = 1 AND name LIKE 'Weidan%',那么语句将会被直接优化为 WHERE name LIKE 'Weidan%' 。这么想想好像直接写 WHERE 1 = 1 除了解析器多了点工作,好像也没什么所谓了。

8.4 计算表达式

WHERE age = 9 + 9 那么解析器会直接给你个 WHERE age = 18。不过如果这些计算方式放在了列名上,比如:

1
2
WHERE -age = -18
WHERE to_days(age) = 18

解析器害怕他也优化错了,所以也会放弃优化,一个简单的记忆就是函数发生在等号后面的常量值上,可以优化,但是如果需要依赖列的计算的话,那就放弃优化了。

8.5 必要时转换外连接到内连接

右外连接会被转换为左外连接,毕竟 左外连接 更好确定 驱动表被驱动表

那如果我们在查询 左外连接 的时候,暗示着我得出的结果中,被驱动表不含 NULL 的值,聪明的暖男解析器将会 GET 到这个信息,把 外连接 优化成 内连接

比如上面的例子中:

1
2
3
4
5
6
7
SELECT * 
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id
AND scp.id = 1;

SELECT *
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id;
AND scp.id IS NOT NULL;

那么上面两个语句中是不是就都暗示解析器中,拿到的 scope 中的 id 不能为空。

所以!直接变成这条语句执行:

1
2
3
4
5
6
SELECT * 
FROM student stu INNER JOIN scope scp ON scp.stu_id = stu.id
AND scp.id = 1;

SELECT *
FROM student stu INNER JOIN scope scp ON scp.stu_id = stu.id;;

九.完

那么这时候上面在数据在 MySQL 数据页中的结构,应该是这样子的:

一.InnoDB表数据

上面聊了这么多这个结构,那个结构的。现在是不是有点好奇,InnoDB 是把数据存在哪里的。答案也很简单,存在一颗 B+ 树种。

InnoDB 中,数据所在的位置没有其他地方,就只有一颗 B+ 树。而我们自己建立的索引,也是一颗 B+ 树。存储完整数据的 B+ 树也叫 聚簇索引,而我们自定义的列索引的 B+ 树,则称为 非聚簇索引

OK,大概明白了这两个东西以后,我们一个一个拆开来说说。

二.B+树是什么

关乎数据结构的东西了,如果觉得很简单,或者说已经学过脑袋有印象,可以跳过这段一个非计算机老男人写的废话。

B+ 树,一切要从一个最简单的例子开始,就是二分查找法。

2.1 二分查找法

我们在聚合的时候,估计都玩过一个游戏,叫做大冒险。确定谁来大冒险,有个方式就是猜数字:由上一轮中的倒霉鬼,来确定一个数字,然后围成一圈的小伙伴们来猜。猜中了,就是下一个倒霉鬼。

那怎么猜的呢,这个倒霉鬼在手机上输入一个数字,盖在地上防止修改。然后从一圈中某个人开始,说一个数字,那么那个站在台上的倒霉蛋就会缩小范围,让下一个人继续猜:

比如数字是69。

那么就有下面的对话:

1
2
3
4
5
6
A: 50
倒霉:50 - 100
B:75
倒霉:50 - 75
C:69
倒霉:恭喜你答对了balabala

这个过程咧,有没有很熟悉,对,就是 二分查找法


那为啥使用二分查找法呢,肯定是因为性能好啊。

那比如我有个 有序 数组:[18, 53, 55, 147, 151, 495, 551, 606, 638, 728]

我一次查询每一个数字,平均次数是 (1+2+3+4+5+6+7+8+9+10)/10 = 5.5次,而使用二分查找法则是 4+3+2+4+3+1+4+3+2+3)/10 = 2.9次。顺序查找最坏的情况也要 10 次,而二分查找最坏是 4 次。这么好的查找效率没有理由不用他。

2.2 二叉查找树

那二分查找和树什么关系,这里就要说到 二叉查找树,根据百度百科的定义:

一棵空树,或者是具有下列性质的二叉树

(1)若左子树不空,则左子树上所有结点的值均小于它的根结点的值;

(2)若右子树不空,则右子树上所有结点的值均大于它的根结点的值;

(3)左、右子树也分别为二叉排序树;

(4)没有键值相等的结点。

那么我现在把上面的数组转换成 二叉查找树,并且给予一个查找的动图:

查询看起来是很方便,但是对数据的增删查改就不是这样的了。有时候,数据插入多了,如果不调整可能会所有都放在了左边或者右边,这样性能就会越来越接近顺序查找。那么,数学家就提出了 平衡二叉树最优二叉树 的定义。

2.3 平衡二叉树

平衡二叉树:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1。这样子就可以让树变得矮胖矮胖的好身材,至于为啥矮胖矮胖才是好身材,因为这样子的话,查询效率才高啊。

比如下面这颗,查找大于 495 的时候,简直就是线性查找的效率了。

image-20200113155115465

那长得丑怎么办,整容(旋转跳跃我闭着眼)啊,不过需要钱(性能)。

那我现在就按照 二叉查找树 那一颗,来插入一个 999,平衡二叉树是在增删改的时候通过旋转来平衡树。

那么有长得帅的读者就要问了,这个跟 B+树 是什么关系。

2.4 B+树

因为 B+树 跟上面的思想差不多,所以,现在就来说说 B+树

B+树 她不是二叉树,但是查询跟 二叉树 差不多,我先放一颗 B+树

这是一颗高度为 2 扇出为 5B+树

那这个怎么实现查找的呢,现在比方说我要查找 606 这条数据:

那么 B+树 是怎么实现新增,然后平衡的呢,跟上面一样发生旋转。旋转的规则如下:

叶子节点是第二层的数据,索引节点是第一层查找时用到的数据。那旋转的情况就是节点页有没有满了:

叶子节点 索引节点 操作
× × 直接插入
× 1. 拆分叶子节点;
2. 中间节点值存入索引节点;
3. 小于中间节点的数据放左叶子,大于或等于放入右叶子。
1. 按照上面情况拆分叶子节点;
2. 然后根据相同的操作再拆分索引节点

第一种就不用演示了,比如新增 1000 直接存入最后边的叶子节点就好了。

接下来我演示第二种,那我就插入 645 这个数字吧。

可以从图中看到,拆分页是多么耗性能的一个东西了,所以我们经常说顺序插入是最好的性能,也要求主键一般呈递增的状态。

但是当左右兄弟节点可能没有满的时候,InnoDB不会急着去拆分数据页,而是会通过旋转的方式来让树达到平衡。这里就不说了。

那如果是第三种情况呢,拆分索引页跟拆分数据页是同样的道理,我就不画出来了,只要知道如果是第三种,性能还要比第二种低一倍,因为不仅拆分数据页,还拆分索引页了。

那如果页中有记录被删除呢,怎么去平衡,这时候就有一个东西叫做 计算因子,如果删除后的页的记录数量小于 计算因子*总页数 的时候,B+ 树会去做 合并操作。那我就继续用这一组数据来做示例。

删除 606 638 以后,B+ 树就会变成下面这个样子:

所以说,B+ 树在发生了修改以后,为了保证查询效率,会对某个一部分的节点进行整容。

三.InnoDB是怎么用B+树

好,结合上一篇的 InnoDB表结构 中的页和数据行的结构,以及刚刚所说的 B+树,现在我们就可以来看看一个表中的数据,是怎样被存储以及怎样被查询的。

从之前我们知道,数据是按照数据页的方式进行存放的,而数据页里面除了记录我们的用户记录行意外,还有一些额外的属性用来表示这一页的类型(所以数据页不仅仅是存放数据的页,也可能是 undo页 等等),而数据行里面也有一些额外的属性来辅助查找。

3.1 查找数据所在的页

之前已经系统的列举过 数据页数据行 的所有属性,现在我就挑在索引中需要用到的属性放在图中就可以了,放上其他的没什么用处,也占用地方。

那我现在就假设,上一节中的叶子节点中的数据(如下图),就是我们一条记录的主键 id,因为我们的记录肯定不止有主键这么简单,所以应该还有其他数据,我暂时使用一些占位的方框做代替。

那么这时候上面在数据在 MySQL 数据页中的结构,应该是这样子的:

叶子节点之间的关系,记录类型中:2 表示最小记录,3 表示最大记录,0 表示用户记录。那如果这棵树没有非叶子节点的话,搜索就是线性的需要一个一个遍历,这时候怎么办咧,B+树 的结构就出现了。

这个结构,就是上面 B+树InnoDB 中数据页的结构了。

3.2 查找真实数据

那么,上面我演示的一个页有三个记录,但其实不止三个记录,我就假设一个页有 16 条记录(其实不止,根据数据的大小)吧。那通过索引只能找到这一页呀,难道要一个一个遍历查询相对应的数据,这时候使用遍历还是有点早了(毕竟遍历并没有那么高效),所以在每个数据页中,又使用了一个槽进行分区。

那现在把镜头切换到一个页中来:

InnoDB 会把一个页中的所有数据根据一个很小的基数,比如 4 个记录为一组,去划分分组。那么每个组在数据页中都有对应的槽来记录分组的最大值。

那按照上面的数据,比如我要查询 55

槽0 = 18,槽1 = 54,槽2 = 64,槽3 = 77。

怎么找呢,类似于上面那个二分查找的游戏,槽018,老大说 18-77 之间,槽154,老大说 54-77,直到最后,确定数字是在 槽1槽2 之间,因为 槽2 = 64 大于 55,而 槽1 = 54 小于 55。所以从 槽1 的最大值 54 开始寻找下一条记录,遍历分组里面的数据行(因为这时候需要遍历的长度已经被切到很小了,所以是时候用遍历来做了,因为用其他算法的次数跟遍历差不多),取出 55 这条数据。

那整个查询过程中,需要跟硬盘交互的并不多,我们知道,InnoDB 是通过数据页来进行硬盘与内存的沟通的,查找效率高就意味着,我需要从硬盘取数据的次数越少。

3.3 删除记录

删除记录时,InnoDB 是怎么做的,就是在之前提到的数据行的头信息的 delete_flag 设置为 1,这条记录就会被放入 垃圾链表,当又需要插入新数据的时候,会判断是否合适放在这个位置,再把原来的数据给覆盖掉。

为什么这么做无非就是减少 硬盘IO。如果真的删除的话,需要去同步硬盘,这个过程并不是很重要,所以先标记就好了。

然后这条记录的上一条记录的 next_record 字段将被重新指向当前删除记录的下一条记录。

四.聚簇索引

那聚簇索引是什么,其实看了上面的图,加点料就可以了:

其实上面的例子我使用的是 id 来做例子,但是每条记录还带有其他所有列的信息(非 NULL)的,如果有 NULL 则会被记录在记录头信息,然后节省了存储数据的空间。

而这种索引,叫做 聚簇索引,意思是什么咧,就是一条记录保存了完整的数据信息,非聚簇则不是。而每条数据都会有一个 primary key,如果没有,InnoDB 会自动加上这个隐藏的列。这也是 MySQL_InnoDB 结构的库保存数据的方式。

所以可以这么说,InnoDB 就是 B+树 构成的。而根节点从建立表开始将会被记录,之后每次查询都会从这个根节点开始查询。

五.非聚簇索引

5.1 简单非聚簇索引

非聚簇索引聚簇索引 有什么区别,区别在于 非聚簇索引 并没有存放整条记录的所有数据,只是存放了 索引列主键 而已。所以当我们通过 非聚簇索引 查询一条数据行的所有列的时候,就需要 回表 去查询其他列的信息了,也就是说需要 两次查询 才完成这个需求。

比方一个表:

1
2
3
4
5
6
CREATE TABLE trbac_user (
id BIGINT auto_increase,
name varchar(50) NOT NULL,
mobile varchar(50) NOT NULL
PRIMARY KEY(id)
)

那我使用 name 来做非聚簇索引,这时候 InnoDB 就有一颗以 name 来查询的 B+树

那我们可以看到,数据行并没有覆盖所有数据,只有 name + id。那比如我们只是查询 name 或者 id 列,就可以直接在这里返回去了。

但是如果我还想要知道 mobile 列的话,这时候就需要在上面那棵树拿到 id,然后再去 id 那棵树查询其他的列信息。

需要从下面的主键的数据页来查询其他数据。

那这里有个什么经验呢…就是我们可以根据经常使用常用的列来建立索引,先拿到 id,然后再去缓存命中,再走数据库~

单列聚簇索引还有个需要注意的点,就是这个列的辨识度要搞,如果这个列只有 MANWOMAN 的话,为这个列建立索引根本就没有什么用处,因为跟全表扫描一样的效率,优化器更偏向于全表扫描,所以切记不要给列值只有寥寥几个的列建立索引,纯属浪费。

5.2 多列非聚簇索引

如果是 name + mobile 联合索引的话,需要注意的点就有点多了:

每个数据页的排序就会变成,先按照 name 进行排序,然后再按照 mobile 进行排序。

5.4 什么时候不会使用索引

1.不满足最左匹配

我想大家都听过 最左匹配原则,就是这个意思,因为我的索引是先根据 name 排序再根据 mobile 进行排序的。那么查询的时候,如果加上 order by 的话,这个排序同样也是派上用场的。

但是如果说查询条件只是写了 WHERE mobile = '13800x1',那上面的索引就完全派不上用场。

怎么说,因为我这个索引是结合了 name 先做排序然后索引的,你只是查询 mobile 的话,就需要遍历整个索引,拿到 id 后还需要回表,这个过程跟直接扫描 聚簇索引 相比,耗费的资源更多,所以,如果一个 name + mobile 的索引,只应用于带有 name 开始的查询条件。因为 name 在前面。这就是 最左匹配原则

2.排序一个列使用升序一个使用降序

比如 WHERE name like 'xxx%' ORDER BY name DESC,mobile ASC,这样的情况只能用到 name 列的索引(也就是所有 mobile 失去了效果),然后再把所有列重新整理,依据 mobile 重新排序。

如果两个都是同一个方向的排序就不会出现这种情况。

3.VARCHAR 匹配前缀查询

WHERE name like '%狗蛋' 这种情况为什么不能使用索引呢,因为你前面未知啊,根据 name 排序的索引查询跟全表扫描有什么区别,还少了 回表 的操作,所以只能全表扫描,取出满足 xxx狗蛋 的数据了。

4.排序用到索引中没有的列

这个理解了 区别在于 非聚簇索引 并没有存放整条记录的所有数据,只是存放了 索引列主键 而已 这句话,就知道为什么了,因为索引中的排序并不适合 SQL 指定的顺序。

5.使用函数修饰值的时候

比如 WHERE LOWER(name) = 'aa',每个列都需要通过函数调用,那就跟全表扫描没什么区别了。


那如果 SQL 语句中,条件出现的顺序没有和多列索引的顺序一致的话,会怎么样:

完全不必担心这个问题,因为 SQL 在执行之前还会交给一个叫做 优化器 的东西进行整理,下面会说到,现在就只要知道他会把 SQL 条件整理成跟索引一样的顺序就好了。

既然多列索引可以帮忙查询条件中满足最左匹配的 SQL 语句,是不是越多越好,答案并不是,我觉得应该根据权重来区分,因为 非聚簇索引 他的索引列并不会像自增 ID 的索引树一样,顺序的插入数据(历史原因导致我们项目使用 UUID 做主键的哭晕在厕所),而是经常的造成 页拆分(也就是上面拆分的动图),所以 非聚簇索引 多了,增删改 需要维护这棵树平衡所做的 整容 就越多,严重的话会影响 增删改 的效率。


5.5 什么时候命中索引

那什么时候用到了索引,自然就是避免上面的情况就可以了,就有这些情况,包括但不局限于:

  1. 条件覆盖了索引的最左列;
  2. 常量查询,WHERE name = 'xxx'
  3. 范围查询,满足 1 的情况下,比如 name > 'AA' 或者 name = 'AA' AND mobile > '13800';
  4. 排序,但是需要用到的排序列是同一个方向进行排序的。
  5. 分组,依然需要满足 1 条件,如果用到了 group by nameInnoDB 会先将相同的 name 放在一起,然后再继续根据其他条件进行运算。

5.6 回表代价

可以这么说,如果回表的代价过高,InnoDB解析器 可能直接决定,不要使用索引,直接全表扫描。

怎么回事,就是 非聚簇 索引他的值都是连在一起的,查询的时候,称为 顺序I/O,而 非聚簇索引 查询出来的 id 并不会连在一起,所以回表去查询 id 数据的时候,称为 随机I/O。而 随机I/O 的效率是很低的,所以当命中索引的数据行总是较多的时候,不如 全表扫描 来的快,所以可能出现 SQL 已经完全命中这个索引但是解析器他就是不使用。

怎么防止这种情况发生,就是让查询索引返回的数量少,比如我们可以 LIMIT 100 去限定只查询 100 条数据,或者我们只要查询索引中出现的列,比如 id 然后再在程序中,根据 id 列表去命中缓存,名不中的再一次性批量查库就可以了。

5.7 文件排序

有时候我们去解析 EXPLAIN 我们的 SQL 语句的时候,会出现 File Sort 类型,这个类型指的是,查询出来的结果不是按照我们 SQL 指定的顺序来的,所以需要在内存中(必要时借助硬盘)来重新对数据进行排序,这个过程就是 File Sort,那如何避免这个类型出现,那就是防止上面不命中索引的情况。

5.8 索引合并

使用等值且命中两个索引的列

比方说我的用户表有 name 索引 和 mobile 索引,那这个条件 WHERE name = 'Weidan' AND mobile = '18588777777' 就可以使用两个索引,然后取 id交集,然后回表查询。但是如果 name > 'Weidan' 这种条件,就不会使用到合并。

但是但是,如果是主键列是范围的话,是可以的,因为他们合并的时候也会操作主键嘛。

不过有没有使用到,还要看成本,如果一个索引命中的数据太多,依然不会使用合并索引。

合并两个索引的列

依然需要满足 列都是等值查询 这个条件,比如 WHERE name = 'Weidan' OR mobile = '18588777777',会合并两个主键再统一回表查询。

范围搜索合并查询

不过如果我们是范围查询,又想使用两个索引怎么办,我们可以把 name > 'Weidan' 以及 monile > '18588777777' 先查询出来 id 然后 union 在一起,再通过 id 去寻找我们想要的数据。

但是还是建议把需要合并的列作为一个 非聚簇 索引来覆盖,效率要高。

七.表连接查询

首先就以最经典的学生、成绩来做示例吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
CREATE TABLE `student` (
`id` int(11) NOT NULL,
`name` varchar(50) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

BEGIN;
INSERT INTO `student` VALUES (1, '狗蛋');
INSERT INTO `student` VALUES (2, '狗剩');
INSERT INTO `student` VALUES (3, '翠花');
COMMIT;

DROP TABLE IF EXISTS `scope`;
CREATE TABLE `scope` (
`id` int(11) NOT NULL,
`subject` varchar(255) DEFAULT NULL,
`scope` int(255) DEFAULT NULL,
`stu_id` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

BEGIN;
INSERT INTO `scope` VALUES (1, '语文', 80, 1);
INSERT INTO `scope` VALUES (2, '数学', 98, 1);
INSERT INTO `scope` VALUES (3, '英语', 59, 2);
INSERT INTO `scope` VALUES (4, '政治', 79, 2);
COMMIT;

7.1 连表查询

那么先列举一个连表很普通的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
SELECT * 
FROM student, scope;

+----+--------+----+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+----+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 2 | 狗剩 | 1 | 语文 | 80 | 1 |
| 3 | 翠花 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 2 | 数学 | 98 | 1 |
| 3 | 翠花 | 2 | 数学 | 98 | 1 |
| 1 | 狗蛋 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 3 | 翠花 | 3 | 英语 | 59 | 2 |
| 1 | 狗蛋 | 4 | 政治 | 79 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
| 3 | 翠花 | 4 | 政治 | 79 | 2 |
+----+--------+----+---------+-------+--------+
12 rows in set (0.00 sec)

可以看到,如果我们不加任何条件,直接就连接两张表的话,取出来的结果就是第一张表所有的数据,去搭配第二张表所有的数据,也就是第一张表有 3 条数据,第二张表有 4 条记录,结果就有 3 ✖ 4 = 12 条记录。这个过程也称为 笛卡尔积。这种情况在日常编程中应该也没人会这么写的吧,仔细想想,如果两张表都是百万级别的数据,笛卡尔积是多恐怖的一件事情。

7.2 内连接

就是由于上面的结果集一般我们都不需要,所以我们会加上一些条件加以限制:

1
2
3
4
5
6
7
8
9
10
11
12
13
SELECT stu.*, scp.*
FROM student stu, scope scp
WHERE stu.id = scp.stu_id;

+----+--------+----+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+----+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
+----+--------+----+---------+-------+--------+
4 rows in set (0.00 sec)

这就是我们想要的结果集,所有参考学生的考试成绩。

InnoDB 是怎样的实现两个表连接的呢,这时候,优化器会根据查询成本确定 驱动表被驱动表

  • 驱动表:先查询的那个表,这里假设是 student 表,所以执行的时候,第一步执行 SELECT * FROM student;

  • 被驱动表:查询出来驱动表所有的数据以后,就会根据驱动表中每一条数据,去执行被驱动表。

    • 比如我现在 student 有三条记录,分别 id1 2 3
    • 然后对三条记录分别执行 SELECT * FROM scope WHERE stu_id = ?
    • 因为 scope 表中不存在 翠花 的成绩,所以 SELECT * FROM scope WHERE stu_id = 3 并没有展示出来,因为这个语句相当于 SELECT * FROM scope WHERE stu_id NOT NULL AND stu_id = 3

那如果上面的语句加上其他条件会怎么样,比如:

1
2
3
4
5
# 查询成绩 90 分以上的学生成绩信息
SELECT stu.*, scp.*
FROM student stu, scope scp
WHERE stu.id = scp.stu_id
AND scp.scope > 90;

那在访问驱动表 student 的时候同上面一样,但是针对驱动表中每一条数据执行被驱动表的时候,就会变成:

SELECT * FROM scope WHERE stu_id NOT NULL AND stu_id = ? AND scope > 90;

一句话就是说,这些条件针对哪个表,在执行那个表的时候就会带上指定的条件。

内连接推荐写法:

1
2
3
SELECT stu.*, scp.*
FROM student stu INNER JOIN scope scp ON stu.id = scp.stu_id
WHERE scp.scope > 90;

效率没有变高,但是可读性变好,在我看来,特定的限定功能就需要放在特定的位置,连接条件在 ON 后面,而查询条件在 WHERE 后面,也可以很好的跟下面的外连接写法统一。

7.3 外连接

外连接的目的是将连接在一起查询的表中,以某个表为主要目的,查询他在另外一个表的所有信息,包括不存在的信息。比如还是上面的例子,翠花她是个坏学生,一个科目都没有参加考试,但是老师如果通过上面的内连接查询来看的话,可能都不知道还有这个人的存在。这就麻烦大了啊,家长上门投诉,我给你钱了,她没参加考试你都不管管的吗。所以这时候,就需要外连接来做这个事情了:

1
2
3
4
5
6
7
8
9
10
11
12
SELECT * 
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id;
+----+--------+------+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+------+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
| 3 | 翠花 | NULL | NULL | NULL | NULL |
+----+--------+------+---------+-------+--------+
5 rows in set (0.00 sec)

这个时候,嗯哼,一抓一个准,没有来考试的,在 scope 表的字段中,显示为 NULL

关于 驱动表被驱动表 的含义已经在上面内连接的内容中指定,那么如果说 内连接 是根据成本来指定 驱动表 的,那 外连接 就是我们来指定驱动表。

驱动表 所有满足我们条件的数据,来查询 被驱动表,如果 被驱动表 不存在 驱动表 中某条数据的关联,显示为 NULL

那么关键点在哪里,就在这个 ON 后面的条件,ON 后面的条件,是指定 被驱动表 中不满足 ON 条件情况下依然要显示的关键。(在内连接中连接条件放 ON 和放 WHERE 效果一样)

如果外连接有几层,比如说三层:

1
2
3
4
SELECT *
FROM teacher t
LEFT JOIN student ON t.id = stu.into
LEFT JOIN scope scp ON stu.id = scope.stu_id;

那么上面这条查询需要怎么走呢,先查询老师所有的学生,得到一张中间表以后,再以这张中间表作为 驱动表 来查询 scope

八.复杂条件查询

MySQL InnoDB 中存在着 SQL解析器,解析器可谓为了查询正确的数据而费尽心思,目的只有一个,就是较快的查询出来正确的数据。其实这里也可以联想到 jvm编译器,为了执行效率比较快,会对字节码进行一些顺序的重新编排,什么 happen before 规则都出来了。

而什么东西查询最快呢,那就是查询常量的时候了。

8.1 整理查询语句

首先就是去除不必要的括号,因为括号如果没有意义存在,那移除了就更有利于优化器的解析了。

比如 WHERE (id = 1) 会被优化成 WHERE id = 1。这个好像也没什么好说的。

复杂一点的 WHERE (id = 1 AND name = 'WEIDAN') AND age = 20 则会被优化成 WHERE id = 1 AND name = 'WEIDAN' AND age = 20

8.2 简化查询条件

WHERE age > 18 AND real_age > age 则直接变成 WHERE age > 18 AND real_age > 18

8.3 删除废话条件

像我之前老师教的,在写 MyBatis Mapper.xml 的时候,因为 WHERE 总是需要动态的条件,所以前面会加一个 WHERE 1 = 1(为啥不用标签,因为标签可读性其实不怎么高)。

那么,如果我写的是 WHERE 1 = 1 AND name LIKE 'Weidan%',那么语句将会被直接优化为 WHERE name LIKE 'Weidan%' 。这么想想好像直接写 WHERE 1 = 1 除了解析器多了点工作,好像也没什么所谓了。

8.4 计算表达式

WHERE age = 9 + 9 那么解析器会直接给你个 WHERE age = 18。不过如果这些计算方式放在了列名上,比如:

1
2
WHERE -age = -18
WHERE to_days(age) = 18

解析器害怕他也优化错了,所以也会放弃优化,一个简单的记忆就是函数发生在等号后面的常量值上,可以优化,但是如果需要依赖列的计算的话,那就放弃优化了。

8.5 必要时转换外连接到内连接

右外连接会被转换为左外连接,毕竟 左外连接 更好确定 驱动表被驱动表

那如果我们在查询 左外连接 的时候,暗示着我得出的结果中,被驱动表不含 NULL 的值,聪明的暖男解析器将会 GET 到这个信息,把 外连接 优化成 内连接

比如上面的例子中:

1
2
3
4
5
6
7
SELECT * 
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id
AND scp.id = 1;

SELECT *
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id;
AND scp.id IS NOT NULL;

那么上面两个语句中是不是就都暗示解析器中,拿到的 scope 中的 id 不能为空。

所以!直接变成这条语句执行:

1
2
3
4
5
6
SELECT * 
FROM student stu INNER JOIN scope scp ON scp.stu_id = stu.id
AND scp.id = 1;

SELECT *
FROM student stu INNER JOIN scope scp ON scp.stu_id = stu.id;;

九.完

叶子节点之间的关系,记录类型中:2 表示最小记录,3 表示最大记录,0 表示用户记录。那如果这棵树没有非叶子节点的话,搜索就是线性的需要一个一个遍历,这时候怎么办咧,B+树 的结构就出现了。

一.InnoDB表数据

上面聊了这么多这个结构,那个结构的。现在是不是有点好奇,InnoDB 是把数据存在哪里的。答案也很简单,存在一颗 B+ 树种。

InnoDB 中,数据所在的位置没有其他地方,就只有一颗 B+ 树。而我们自己建立的索引,也是一颗 B+ 树。存储完整数据的 B+ 树也叫 聚簇索引,而我们自定义的列索引的 B+ 树,则称为 非聚簇索引

OK,大概明白了这两个东西以后,我们一个一个拆开来说说。

二.B+树是什么

关乎数据结构的东西了,如果觉得很简单,或者说已经学过脑袋有印象,可以跳过这段一个非计算机老男人写的废话。

B+ 树,一切要从一个最简单的例子开始,就是二分查找法。

2.1 二分查找法

我们在聚合的时候,估计都玩过一个游戏,叫做大冒险。确定谁来大冒险,有个方式就是猜数字:由上一轮中的倒霉鬼,来确定一个数字,然后围成一圈的小伙伴们来猜。猜中了,就是下一个倒霉鬼。

那怎么猜的呢,这个倒霉鬼在手机上输入一个数字,盖在地上防止修改。然后从一圈中某个人开始,说一个数字,那么那个站在台上的倒霉蛋就会缩小范围,让下一个人继续猜:

比如数字是69。

那么就有下面的对话:

1
2
3
4
5
6
A: 50
倒霉:50 - 100
B:75
倒霉:50 - 75
C:69
倒霉:恭喜你答对了balabala

这个过程咧,有没有很熟悉,对,就是 二分查找法


那为啥使用二分查找法呢,肯定是因为性能好啊。

那比如我有个 有序 数组:[18, 53, 55, 147, 151, 495, 551, 606, 638, 728]

我一次查询每一个数字,平均次数是 (1+2+3+4+5+6+7+8+9+10)/10 = 5.5次,而使用二分查找法则是 4+3+2+4+3+1+4+3+2+3)/10 = 2.9次。顺序查找最坏的情况也要 10 次,而二分查找最坏是 4 次。这么好的查找效率没有理由不用他。

2.2 二叉查找树

那二分查找和树什么关系,这里就要说到 二叉查找树,根据百度百科的定义:

一棵空树,或者是具有下列性质的二叉树

(1)若左子树不空,则左子树上所有结点的值均小于它的根结点的值;

(2)若右子树不空,则右子树上所有结点的值均大于它的根结点的值;

(3)左、右子树也分别为二叉排序树;

(4)没有键值相等的结点。

那么我现在把上面的数组转换成 二叉查找树,并且给予一个查找的动图:

查询看起来是很方便,但是对数据的增删查改就不是这样的了。有时候,数据插入多了,如果不调整可能会所有都放在了左边或者右边,这样性能就会越来越接近顺序查找。那么,数学家就提出了 平衡二叉树最优二叉树 的定义。

2.3 平衡二叉树

平衡二叉树:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1。这样子就可以让树变得矮胖矮胖的好身材,至于为啥矮胖矮胖才是好身材,因为这样子的话,查询效率才高啊。

比如下面这颗,查找大于 495 的时候,简直就是线性查找的效率了。

image-20200113155115465

那长得丑怎么办,整容(旋转跳跃我闭着眼)啊,不过需要钱(性能)。

那我现在就按照 二叉查找树 那一颗,来插入一个 999,平衡二叉树是在增删改的时候通过旋转来平衡树。

那么有长得帅的读者就要问了,这个跟 B+树 是什么关系。

2.4 B+树

因为 B+树 跟上面的思想差不多,所以,现在就来说说 B+树

B+树 她不是二叉树,但是查询跟 二叉树 差不多,我先放一颗 B+树

这是一颗高度为 2 扇出为 5B+树

那这个怎么实现查找的呢,现在比方说我要查找 606 这条数据:

那么 B+树 是怎么实现新增,然后平衡的呢,跟上面一样发生旋转。旋转的规则如下:

叶子节点是第二层的数据,索引节点是第一层查找时用到的数据。那旋转的情况就是节点页有没有满了:

叶子节点 索引节点 操作
× × 直接插入
× 1. 拆分叶子节点;
2. 中间节点值存入索引节点;
3. 小于中间节点的数据放左叶子,大于或等于放入右叶子。
1. 按照上面情况拆分叶子节点;
2. 然后根据相同的操作再拆分索引节点

第一种就不用演示了,比如新增 1000 直接存入最后边的叶子节点就好了。

接下来我演示第二种,那我就插入 645 这个数字吧。

可以从图中看到,拆分页是多么耗性能的一个东西了,所以我们经常说顺序插入是最好的性能,也要求主键一般呈递增的状态。

但是当左右兄弟节点可能没有满的时候,InnoDB不会急着去拆分数据页,而是会通过旋转的方式来让树达到平衡。这里就不说了。

那如果是第三种情况呢,拆分索引页跟拆分数据页是同样的道理,我就不画出来了,只要知道如果是第三种,性能还要比第二种低一倍,因为不仅拆分数据页,还拆分索引页了。

那如果页中有记录被删除呢,怎么去平衡,这时候就有一个东西叫做 计算因子,如果删除后的页的记录数量小于 计算因子*总页数 的时候,B+ 树会去做 合并操作。那我就继续用这一组数据来做示例。

删除 606 638 以后,B+ 树就会变成下面这个样子:

所以说,B+ 树在发生了修改以后,为了保证查询效率,会对某个一部分的节点进行整容。

三.InnoDB是怎么用B+树

好,结合上一篇的 InnoDB表结构 中的页和数据行的结构,以及刚刚所说的 B+树,现在我们就可以来看看一个表中的数据,是怎样被存储以及怎样被查询的。

从之前我们知道,数据是按照数据页的方式进行存放的,而数据页里面除了记录我们的用户记录行意外,还有一些额外的属性用来表示这一页的类型(所以数据页不仅仅是存放数据的页,也可能是 undo页 等等),而数据行里面也有一些额外的属性来辅助查找。

3.1 查找数据所在的页

之前已经系统的列举过 数据页数据行 的所有属性,现在我就挑在索引中需要用到的属性放在图中就可以了,放上其他的没什么用处,也占用地方。

那我现在就假设,上一节中的叶子节点中的数据(如下图),就是我们一条记录的主键 id,因为我们的记录肯定不止有主键这么简单,所以应该还有其他数据,我暂时使用一些占位的方框做代替。

那么这时候上面在数据在 MySQL 数据页中的结构,应该是这样子的:

叶子节点之间的关系,记录类型中:2 表示最小记录,3 表示最大记录,0 表示用户记录。那如果这棵树没有非叶子节点的话,搜索就是线性的需要一个一个遍历,这时候怎么办咧,B+树 的结构就出现了。

这个结构,就是上面 B+树InnoDB 中数据页的结构了。

3.2 查找真实数据

那么,上面我演示的一个页有三个记录,但其实不止三个记录,我就假设一个页有 16 条记录(其实不止,根据数据的大小)吧。那通过索引只能找到这一页呀,难道要一个一个遍历查询相对应的数据,这时候使用遍历还是有点早了(毕竟遍历并没有那么高效),所以在每个数据页中,又使用了一个槽进行分区。

那现在把镜头切换到一个页中来:

InnoDB 会把一个页中的所有数据根据一个很小的基数,比如 4 个记录为一组,去划分分组。那么每个组在数据页中都有对应的槽来记录分组的最大值。

那按照上面的数据,比如我要查询 55

槽0 = 18,槽1 = 54,槽2 = 64,槽3 = 77。

怎么找呢,类似于上面那个二分查找的游戏,槽018,老大说 18-77 之间,槽154,老大说 54-77,直到最后,确定数字是在 槽1槽2 之间,因为 槽2 = 64 大于 55,而 槽1 = 54 小于 55。所以从 槽1 的最大值 54 开始寻找下一条记录,遍历分组里面的数据行(因为这时候需要遍历的长度已经被切到很小了,所以是时候用遍历来做了,因为用其他算法的次数跟遍历差不多),取出 55 这条数据。

那整个查询过程中,需要跟硬盘交互的并不多,我们知道,InnoDB 是通过数据页来进行硬盘与内存的沟通的,查找效率高就意味着,我需要从硬盘取数据的次数越少。

3.3 删除记录

删除记录时,InnoDB 是怎么做的,就是在之前提到的数据行的头信息的 delete_flag 设置为 1,这条记录就会被放入 垃圾链表,当又需要插入新数据的时候,会判断是否合适放在这个位置,再把原来的数据给覆盖掉。

为什么这么做无非就是减少 硬盘IO。如果真的删除的话,需要去同步硬盘,这个过程并不是很重要,所以先标记就好了。

然后这条记录的上一条记录的 next_record 字段将被重新指向当前删除记录的下一条记录。

四.聚簇索引

那聚簇索引是什么,其实看了上面的图,加点料就可以了:

其实上面的例子我使用的是 id 来做例子,但是每条记录还带有其他所有列的信息(非 NULL)的,如果有 NULL 则会被记录在记录头信息,然后节省了存储数据的空间。

而这种索引,叫做 聚簇索引,意思是什么咧,就是一条记录保存了完整的数据信息,非聚簇则不是。而每条数据都会有一个 primary key,如果没有,InnoDB 会自动加上这个隐藏的列。这也是 MySQL_InnoDB 结构的库保存数据的方式。

所以可以这么说,InnoDB 就是 B+树 构成的。而根节点从建立表开始将会被记录,之后每次查询都会从这个根节点开始查询。

五.非聚簇索引

5.1 简单非聚簇索引

非聚簇索引聚簇索引 有什么区别,区别在于 非聚簇索引 并没有存放整条记录的所有数据,只是存放了 索引列主键 而已。所以当我们通过 非聚簇索引 查询一条数据行的所有列的时候,就需要 回表 去查询其他列的信息了,也就是说需要 两次查询 才完成这个需求。

比方一个表:

1
2
3
4
5
6
CREATE TABLE trbac_user (
id BIGINT auto_increase,
name varchar(50) NOT NULL,
mobile varchar(50) NOT NULL
PRIMARY KEY(id)
)

那我使用 name 来做非聚簇索引,这时候 InnoDB 就有一颗以 name 来查询的 B+树

那我们可以看到,数据行并没有覆盖所有数据,只有 name + id。那比如我们只是查询 name 或者 id 列,就可以直接在这里返回去了。

但是如果我还想要知道 mobile 列的话,这时候就需要在上面那棵树拿到 id,然后再去 id 那棵树查询其他的列信息。

需要从下面的主键的数据页来查询其他数据。

那这里有个什么经验呢…就是我们可以根据经常使用常用的列来建立索引,先拿到 id,然后再去缓存命中,再走数据库~

单列聚簇索引还有个需要注意的点,就是这个列的辨识度要搞,如果这个列只有 MANWOMAN 的话,为这个列建立索引根本就没有什么用处,因为跟全表扫描一样的效率,优化器更偏向于全表扫描,所以切记不要给列值只有寥寥几个的列建立索引,纯属浪费。

5.2 多列非聚簇索引

如果是 name + mobile 联合索引的话,需要注意的点就有点多了:

每个数据页的排序就会变成,先按照 name 进行排序,然后再按照 mobile 进行排序。

5.4 什么时候不会使用索引

1.不满足最左匹配

我想大家都听过 最左匹配原则,就是这个意思,因为我的索引是先根据 name 排序再根据 mobile 进行排序的。那么查询的时候,如果加上 order by 的话,这个排序同样也是派上用场的。

但是如果说查询条件只是写了 WHERE mobile = '13800x1',那上面的索引就完全派不上用场。

怎么说,因为我这个索引是结合了 name 先做排序然后索引的,你只是查询 mobile 的话,就需要遍历整个索引,拿到 id 后还需要回表,这个过程跟直接扫描 聚簇索引 相比,耗费的资源更多,所以,如果一个 name + mobile 的索引,只应用于带有 name 开始的查询条件。因为 name 在前面。这就是 最左匹配原则

2.排序一个列使用升序一个使用降序

比如 WHERE name like 'xxx%' ORDER BY name DESC,mobile ASC,这样的情况只能用到 name 列的索引(也就是所有 mobile 失去了效果),然后再把所有列重新整理,依据 mobile 重新排序。

如果两个都是同一个方向的排序就不会出现这种情况。

3.VARCHAR 匹配前缀查询

WHERE name like '%狗蛋' 这种情况为什么不能使用索引呢,因为你前面未知啊,根据 name 排序的索引查询跟全表扫描有什么区别,还少了 回表 的操作,所以只能全表扫描,取出满足 xxx狗蛋 的数据了。

4.排序用到索引中没有的列

这个理解了 区别在于 非聚簇索引 并没有存放整条记录的所有数据,只是存放了 索引列主键 而已 这句话,就知道为什么了,因为索引中的排序并不适合 SQL 指定的顺序。

5.使用函数修饰值的时候

比如 WHERE LOWER(name) = 'aa',每个列都需要通过函数调用,那就跟全表扫描没什么区别了。


那如果 SQL 语句中,条件出现的顺序没有和多列索引的顺序一致的话,会怎么样:

完全不必担心这个问题,因为 SQL 在执行之前还会交给一个叫做 优化器 的东西进行整理,下面会说到,现在就只要知道他会把 SQL 条件整理成跟索引一样的顺序就好了。

既然多列索引可以帮忙查询条件中满足最左匹配的 SQL 语句,是不是越多越好,答案并不是,我觉得应该根据权重来区分,因为 非聚簇索引 他的索引列并不会像自增 ID 的索引树一样,顺序的插入数据(历史原因导致我们项目使用 UUID 做主键的哭晕在厕所),而是经常的造成 页拆分(也就是上面拆分的动图),所以 非聚簇索引 多了,增删改 需要维护这棵树平衡所做的 整容 就越多,严重的话会影响 增删改 的效率。


5.5 什么时候命中索引

那什么时候用到了索引,自然就是避免上面的情况就可以了,就有这些情况,包括但不局限于:

  1. 条件覆盖了索引的最左列;
  2. 常量查询,WHERE name = 'xxx'
  3. 范围查询,满足 1 的情况下,比如 name > 'AA' 或者 name = 'AA' AND mobile > '13800';
  4. 排序,但是需要用到的排序列是同一个方向进行排序的。
  5. 分组,依然需要满足 1 条件,如果用到了 group by nameInnoDB 会先将相同的 name 放在一起,然后再继续根据其他条件进行运算。

5.6 回表代价

可以这么说,如果回表的代价过高,InnoDB解析器 可能直接决定,不要使用索引,直接全表扫描。

怎么回事,就是 非聚簇 索引他的值都是连在一起的,查询的时候,称为 顺序I/O,而 非聚簇索引 查询出来的 id 并不会连在一起,所以回表去查询 id 数据的时候,称为 随机I/O。而 随机I/O 的效率是很低的,所以当命中索引的数据行总是较多的时候,不如 全表扫描 来的快,所以可能出现 SQL 已经完全命中这个索引但是解析器他就是不使用。

怎么防止这种情况发生,就是让查询索引返回的数量少,比如我们可以 LIMIT 100 去限定只查询 100 条数据,或者我们只要查询索引中出现的列,比如 id 然后再在程序中,根据 id 列表去命中缓存,名不中的再一次性批量查库就可以了。

5.7 文件排序

有时候我们去解析 EXPLAIN 我们的 SQL 语句的时候,会出现 File Sort 类型,这个类型指的是,查询出来的结果不是按照我们 SQL 指定的顺序来的,所以需要在内存中(必要时借助硬盘)来重新对数据进行排序,这个过程就是 File Sort,那如何避免这个类型出现,那就是防止上面不命中索引的情况。

5.8 索引合并

使用等值且命中两个索引的列

比方说我的用户表有 name 索引 和 mobile 索引,那这个条件 WHERE name = 'Weidan' AND mobile = '18588777777' 就可以使用两个索引,然后取 id交集,然后回表查询。但是如果 name > 'Weidan' 这种条件,就不会使用到合并。

但是但是,如果是主键列是范围的话,是可以的,因为他们合并的时候也会操作主键嘛。

不过有没有使用到,还要看成本,如果一个索引命中的数据太多,依然不会使用合并索引。

合并两个索引的列

依然需要满足 列都是等值查询 这个条件,比如 WHERE name = 'Weidan' OR mobile = '18588777777',会合并两个主键再统一回表查询。

范围搜索合并查询

不过如果我们是范围查询,又想使用两个索引怎么办,我们可以把 name > 'Weidan' 以及 monile > '18588777777' 先查询出来 id 然后 union 在一起,再通过 id 去寻找我们想要的数据。

但是还是建议把需要合并的列作为一个 非聚簇 索引来覆盖,效率要高。

七.表连接查询

首先就以最经典的学生、成绩来做示例吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
CREATE TABLE `student` (
`id` int(11) NOT NULL,
`name` varchar(50) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

BEGIN;
INSERT INTO `student` VALUES (1, '狗蛋');
INSERT INTO `student` VALUES (2, '狗剩');
INSERT INTO `student` VALUES (3, '翠花');
COMMIT;

DROP TABLE IF EXISTS `scope`;
CREATE TABLE `scope` (
`id` int(11) NOT NULL,
`subject` varchar(255) DEFAULT NULL,
`scope` int(255) DEFAULT NULL,
`stu_id` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

BEGIN;
INSERT INTO `scope` VALUES (1, '语文', 80, 1);
INSERT INTO `scope` VALUES (2, '数学', 98, 1);
INSERT INTO `scope` VALUES (3, '英语', 59, 2);
INSERT INTO `scope` VALUES (4, '政治', 79, 2);
COMMIT;

7.1 连表查询

那么先列举一个连表很普通的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
SELECT * 
FROM student, scope;

+----+--------+----+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+----+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 2 | 狗剩 | 1 | 语文 | 80 | 1 |
| 3 | 翠花 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 2 | 数学 | 98 | 1 |
| 3 | 翠花 | 2 | 数学 | 98 | 1 |
| 1 | 狗蛋 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 3 | 翠花 | 3 | 英语 | 59 | 2 |
| 1 | 狗蛋 | 4 | 政治 | 79 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
| 3 | 翠花 | 4 | 政治 | 79 | 2 |
+----+--------+----+---------+-------+--------+
12 rows in set (0.00 sec)

可以看到,如果我们不加任何条件,直接就连接两张表的话,取出来的结果就是第一张表所有的数据,去搭配第二张表所有的数据,也就是第一张表有 3 条数据,第二张表有 4 条记录,结果就有 3 ✖ 4 = 12 条记录。这个过程也称为 笛卡尔积。这种情况在日常编程中应该也没人会这么写的吧,仔细想想,如果两张表都是百万级别的数据,笛卡尔积是多恐怖的一件事情。

7.2 内连接

就是由于上面的结果集一般我们都不需要,所以我们会加上一些条件加以限制:

1
2
3
4
5
6
7
8
9
10
11
12
13
SELECT stu.*, scp.*
FROM student stu, scope scp
WHERE stu.id = scp.stu_id;

+----+--------+----+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+----+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
+----+--------+----+---------+-------+--------+
4 rows in set (0.00 sec)

这就是我们想要的结果集,所有参考学生的考试成绩。

InnoDB 是怎样的实现两个表连接的呢,这时候,优化器会根据查询成本确定 驱动表被驱动表

  • 驱动表:先查询的那个表,这里假设是 student 表,所以执行的时候,第一步执行 SELECT * FROM student;

  • 被驱动表:查询出来驱动表所有的数据以后,就会根据驱动表中每一条数据,去执行被驱动表。

    • 比如我现在 student 有三条记录,分别 id1 2 3
    • 然后对三条记录分别执行 SELECT * FROM scope WHERE stu_id = ?
    • 因为 scope 表中不存在 翠花 的成绩,所以 SELECT * FROM scope WHERE stu_id = 3 并没有展示出来,因为这个语句相当于 SELECT * FROM scope WHERE stu_id NOT NULL AND stu_id = 3

那如果上面的语句加上其他条件会怎么样,比如:

1
2
3
4
5
# 查询成绩 90 分以上的学生成绩信息
SELECT stu.*, scp.*
FROM student stu, scope scp
WHERE stu.id = scp.stu_id
AND scp.scope > 90;

那在访问驱动表 student 的时候同上面一样,但是针对驱动表中每一条数据执行被驱动表的时候,就会变成:

SELECT * FROM scope WHERE stu_id NOT NULL AND stu_id = ? AND scope > 90;

一句话就是说,这些条件针对哪个表,在执行那个表的时候就会带上指定的条件。

内连接推荐写法:

1
2
3
SELECT stu.*, scp.*
FROM student stu INNER JOIN scope scp ON stu.id = scp.stu_id
WHERE scp.scope > 90;

效率没有变高,但是可读性变好,在我看来,特定的限定功能就需要放在特定的位置,连接条件在 ON 后面,而查询条件在 WHERE 后面,也可以很好的跟下面的外连接写法统一。

7.3 外连接

外连接的目的是将连接在一起查询的表中,以某个表为主要目的,查询他在另外一个表的所有信息,包括不存在的信息。比如还是上面的例子,翠花她是个坏学生,一个科目都没有参加考试,但是老师如果通过上面的内连接查询来看的话,可能都不知道还有这个人的存在。这就麻烦大了啊,家长上门投诉,我给你钱了,她没参加考试你都不管管的吗。所以这时候,就需要外连接来做这个事情了:

1
2
3
4
5
6
7
8
9
10
11
12
SELECT * 
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id;
+----+--------+------+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+------+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
| 3 | 翠花 | NULL | NULL | NULL | NULL |
+----+--------+------+---------+-------+--------+
5 rows in set (0.00 sec)

这个时候,嗯哼,一抓一个准,没有来考试的,在 scope 表的字段中,显示为 NULL

关于 驱动表被驱动表 的含义已经在上面内连接的内容中指定,那么如果说 内连接 是根据成本来指定 驱动表 的,那 外连接 就是我们来指定驱动表。

驱动表 所有满足我们条件的数据,来查询 被驱动表,如果 被驱动表 不存在 驱动表 中某条数据的关联,显示为 NULL

那么关键点在哪里,就在这个 ON 后面的条件,ON 后面的条件,是指定 被驱动表 中不满足 ON 条件情况下依然要显示的关键。(在内连接中连接条件放 ON 和放 WHERE 效果一样)

如果外连接有几层,比如说三层:

1
2
3
4
SELECT *
FROM teacher t
LEFT JOIN student ON t.id = stu.into
LEFT JOIN scope scp ON stu.id = scope.stu_id;

那么上面这条查询需要怎么走呢,先查询老师所有的学生,得到一张中间表以后,再以这张中间表作为 驱动表 来查询 scope

八.复杂条件查询

MySQL InnoDB 中存在着 SQL解析器,解析器可谓为了查询正确的数据而费尽心思,目的只有一个,就是较快的查询出来正确的数据。其实这里也可以联想到 jvm编译器,为了执行效率比较快,会对字节码进行一些顺序的重新编排,什么 happen before 规则都出来了。

而什么东西查询最快呢,那就是查询常量的时候了。

8.1 整理查询语句

首先就是去除不必要的括号,因为括号如果没有意义存在,那移除了就更有利于优化器的解析了。

比如 WHERE (id = 1) 会被优化成 WHERE id = 1。这个好像也没什么好说的。

复杂一点的 WHERE (id = 1 AND name = 'WEIDAN') AND age = 20 则会被优化成 WHERE id = 1 AND name = 'WEIDAN' AND age = 20

8.2 简化查询条件

WHERE age > 18 AND real_age > age 则直接变成 WHERE age > 18 AND real_age > 18

8.3 删除废话条件

像我之前老师教的,在写 MyBatis Mapper.xml 的时候,因为 WHERE 总是需要动态的条件,所以前面会加一个 WHERE 1 = 1(为啥不用标签,因为标签可读性其实不怎么高)。

那么,如果我写的是 WHERE 1 = 1 AND name LIKE 'Weidan%',那么语句将会被直接优化为 WHERE name LIKE 'Weidan%' 。这么想想好像直接写 WHERE 1 = 1 除了解析器多了点工作,好像也没什么所谓了。

8.4 计算表达式

WHERE age = 9 + 9 那么解析器会直接给你个 WHERE age = 18。不过如果这些计算方式放在了列名上,比如:

1
2
WHERE -age = -18
WHERE to_days(age) = 18

解析器害怕他也优化错了,所以也会放弃优化,一个简单的记忆就是函数发生在等号后面的常量值上,可以优化,但是如果需要依赖列的计算的话,那就放弃优化了。

8.5 必要时转换外连接到内连接

右外连接会被转换为左外连接,毕竟 左外连接 更好确定 驱动表被驱动表

那如果我们在查询 左外连接 的时候,暗示着我得出的结果中,被驱动表不含 NULL 的值,聪明的暖男解析器将会 GET 到这个信息,把 外连接 优化成 内连接

比如上面的例子中:

1
2
3
4
5
6
7
SELECT * 
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id
AND scp.id = 1;

SELECT *
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id;
AND scp.id IS NOT NULL;

那么上面两个语句中是不是就都暗示解析器中,拿到的 scope 中的 id 不能为空。

所以!直接变成这条语句执行:

1
2
3
4
5
6
SELECT * 
FROM student stu INNER JOIN scope scp ON scp.stu_id = stu.id
AND scp.id = 1;

SELECT *
FROM student stu INNER JOIN scope scp ON scp.stu_id = stu.id;;

九.完

这个结构,就是上面 B+树InnoDB 中数据页的结构了。

3.2 查找真实数据

那么,上面我演示的一个页有三个记录,但其实不止三个记录,我就假设一个页有 16 条记录(其实不止,根据数据的大小)吧。那通过索引只能找到这一页呀,难道要一个一个遍历查询相对应的数据,这时候使用遍历还是有点早了(毕竟遍历并没有那么高效),所以在每个数据页中,又使用了一个槽进行分区。

那现在把镜头切换到一个页中来:

一.InnoDB表数据

上面聊了这么多这个结构,那个结构的。现在是不是有点好奇,InnoDB 是把数据存在哪里的。答案也很简单,存在一颗 B+ 树种。

InnoDB 中,数据所在的位置没有其他地方,就只有一颗 B+ 树。而我们自己建立的索引,也是一颗 B+ 树。存储完整数据的 B+ 树也叫 聚簇索引,而我们自定义的列索引的 B+ 树,则称为 非聚簇索引

OK,大概明白了这两个东西以后,我们一个一个拆开来说说。

二.B+树是什么

关乎数据结构的东西了,如果觉得很简单,或者说已经学过脑袋有印象,可以跳过这段一个非计算机老男人写的废话。

B+ 树,一切要从一个最简单的例子开始,就是二分查找法。

2.1 二分查找法

我们在聚合的时候,估计都玩过一个游戏,叫做大冒险。确定谁来大冒险,有个方式就是猜数字:由上一轮中的倒霉鬼,来确定一个数字,然后围成一圈的小伙伴们来猜。猜中了,就是下一个倒霉鬼。

那怎么猜的呢,这个倒霉鬼在手机上输入一个数字,盖在地上防止修改。然后从一圈中某个人开始,说一个数字,那么那个站在台上的倒霉蛋就会缩小范围,让下一个人继续猜:

比如数字是69。

那么就有下面的对话:

1
2
3
4
5
6
A: 50
倒霉:50 - 100
B:75
倒霉:50 - 75
C:69
倒霉:恭喜你答对了balabala

这个过程咧,有没有很熟悉,对,就是 二分查找法


那为啥使用二分查找法呢,肯定是因为性能好啊。

那比如我有个 有序 数组:[18, 53, 55, 147, 151, 495, 551, 606, 638, 728]

我一次查询每一个数字,平均次数是 (1+2+3+4+5+6+7+8+9+10)/10 = 5.5次,而使用二分查找法则是 4+3+2+4+3+1+4+3+2+3)/10 = 2.9次。顺序查找最坏的情况也要 10 次,而二分查找最坏是 4 次。这么好的查找效率没有理由不用他。

2.2 二叉查找树

那二分查找和树什么关系,这里就要说到 二叉查找树,根据百度百科的定义:

一棵空树,或者是具有下列性质的二叉树

(1)若左子树不空,则左子树上所有结点的值均小于它的根结点的值;

(2)若右子树不空,则右子树上所有结点的值均大于它的根结点的值;

(3)左、右子树也分别为二叉排序树;

(4)没有键值相等的结点。

那么我现在把上面的数组转换成 二叉查找树,并且给予一个查找的动图:

查询看起来是很方便,但是对数据的增删查改就不是这样的了。有时候,数据插入多了,如果不调整可能会所有都放在了左边或者右边,这样性能就会越来越接近顺序查找。那么,数学家就提出了 平衡二叉树最优二叉树 的定义。

2.3 平衡二叉树

平衡二叉树:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1。这样子就可以让树变得矮胖矮胖的好身材,至于为啥矮胖矮胖才是好身材,因为这样子的话,查询效率才高啊。

比如下面这颗,查找大于 495 的时候,简直就是线性查找的效率了。

image-20200113155115465

那长得丑怎么办,整容(旋转跳跃我闭着眼)啊,不过需要钱(性能)。

那我现在就按照 二叉查找树 那一颗,来插入一个 999,平衡二叉树是在增删改的时候通过旋转来平衡树。

那么有长得帅的读者就要问了,这个跟 B+树 是什么关系。

2.4 B+树

因为 B+树 跟上面的思想差不多,所以,现在就来说说 B+树

B+树 她不是二叉树,但是查询跟 二叉树 差不多,我先放一颗 B+树

这是一颗高度为 2 扇出为 5B+树

那这个怎么实现查找的呢,现在比方说我要查找 606 这条数据:

那么 B+树 是怎么实现新增,然后平衡的呢,跟上面一样发生旋转。旋转的规则如下:

叶子节点是第二层的数据,索引节点是第一层查找时用到的数据。那旋转的情况就是节点页有没有满了:

叶子节点 索引节点 操作
× × 直接插入
× 1. 拆分叶子节点;
2. 中间节点值存入索引节点;
3. 小于中间节点的数据放左叶子,大于或等于放入右叶子。
1. 按照上面情况拆分叶子节点;
2. 然后根据相同的操作再拆分索引节点

第一种就不用演示了,比如新增 1000 直接存入最后边的叶子节点就好了。

接下来我演示第二种,那我就插入 645 这个数字吧。

可以从图中看到,拆分页是多么耗性能的一个东西了,所以我们经常说顺序插入是最好的性能,也要求主键一般呈递增的状态。

但是当左右兄弟节点可能没有满的时候,InnoDB不会急着去拆分数据页,而是会通过旋转的方式来让树达到平衡。这里就不说了。

那如果是第三种情况呢,拆分索引页跟拆分数据页是同样的道理,我就不画出来了,只要知道如果是第三种,性能还要比第二种低一倍,因为不仅拆分数据页,还拆分索引页了。

那如果页中有记录被删除呢,怎么去平衡,这时候就有一个东西叫做 计算因子,如果删除后的页的记录数量小于 计算因子*总页数 的时候,B+ 树会去做 合并操作。那我就继续用这一组数据来做示例。

删除 606 638 以后,B+ 树就会变成下面这个样子:

所以说,B+ 树在发生了修改以后,为了保证查询效率,会对某个一部分的节点进行整容。

三.InnoDB是怎么用B+树

好,结合上一篇的 InnoDB表结构 中的页和数据行的结构,以及刚刚所说的 B+树,现在我们就可以来看看一个表中的数据,是怎样被存储以及怎样被查询的。

从之前我们知道,数据是按照数据页的方式进行存放的,而数据页里面除了记录我们的用户记录行意外,还有一些额外的属性用来表示这一页的类型(所以数据页不仅仅是存放数据的页,也可能是 undo页 等等),而数据行里面也有一些额外的属性来辅助查找。

3.1 查找数据所在的页

之前已经系统的列举过 数据页数据行 的所有属性,现在我就挑在索引中需要用到的属性放在图中就可以了,放上其他的没什么用处,也占用地方。

那我现在就假设,上一节中的叶子节点中的数据(如下图),就是我们一条记录的主键 id,因为我们的记录肯定不止有主键这么简单,所以应该还有其他数据,我暂时使用一些占位的方框做代替。

那么这时候上面在数据在 MySQL 数据页中的结构,应该是这样子的:

叶子节点之间的关系,记录类型中:2 表示最小记录,3 表示最大记录,0 表示用户记录。那如果这棵树没有非叶子节点的话,搜索就是线性的需要一个一个遍历,这时候怎么办咧,B+树 的结构就出现了。

这个结构,就是上面 B+树InnoDB 中数据页的结构了。

3.2 查找真实数据

那么,上面我演示的一个页有三个记录,但其实不止三个记录,我就假设一个页有 16 条记录(其实不止,根据数据的大小)吧。那通过索引只能找到这一页呀,难道要一个一个遍历查询相对应的数据,这时候使用遍历还是有点早了(毕竟遍历并没有那么高效),所以在每个数据页中,又使用了一个槽进行分区。

那现在把镜头切换到一个页中来:

InnoDB 会把一个页中的所有数据根据一个很小的基数,比如 4 个记录为一组,去划分分组。那么每个组在数据页中都有对应的槽来记录分组的最大值。

那按照上面的数据,比如我要查询 55

槽0 = 18,槽1 = 54,槽2 = 64,槽3 = 77。

怎么找呢,类似于上面那个二分查找的游戏,槽018,老大说 18-77 之间,槽154,老大说 54-77,直到最后,确定数字是在 槽1槽2 之间,因为 槽2 = 64 大于 55,而 槽1 = 54 小于 55。所以从 槽1 的最大值 54 开始寻找下一条记录,遍历分组里面的数据行(因为这时候需要遍历的长度已经被切到很小了,所以是时候用遍历来做了,因为用其他算法的次数跟遍历差不多),取出 55 这条数据。

那整个查询过程中,需要跟硬盘交互的并不多,我们知道,InnoDB 是通过数据页来进行硬盘与内存的沟通的,查找效率高就意味着,我需要从硬盘取数据的次数越少。

3.3 删除记录

删除记录时,InnoDB 是怎么做的,就是在之前提到的数据行的头信息的 delete_flag 设置为 1,这条记录就会被放入 垃圾链表,当又需要插入新数据的时候,会判断是否合适放在这个位置,再把原来的数据给覆盖掉。

为什么这么做无非就是减少 硬盘IO。如果真的删除的话,需要去同步硬盘,这个过程并不是很重要,所以先标记就好了。

然后这条记录的上一条记录的 next_record 字段将被重新指向当前删除记录的下一条记录。

四.聚簇索引

那聚簇索引是什么,其实看了上面的图,加点料就可以了:

其实上面的例子我使用的是 id 来做例子,但是每条记录还带有其他所有列的信息(非 NULL)的,如果有 NULL 则会被记录在记录头信息,然后节省了存储数据的空间。

而这种索引,叫做 聚簇索引,意思是什么咧,就是一条记录保存了完整的数据信息,非聚簇则不是。而每条数据都会有一个 primary key,如果没有,InnoDB 会自动加上这个隐藏的列。这也是 MySQL_InnoDB 结构的库保存数据的方式。

所以可以这么说,InnoDB 就是 B+树 构成的。而根节点从建立表开始将会被记录,之后每次查询都会从这个根节点开始查询。

五.非聚簇索引

5.1 简单非聚簇索引

非聚簇索引聚簇索引 有什么区别,区别在于 非聚簇索引 并没有存放整条记录的所有数据,只是存放了 索引列主键 而已。所以当我们通过 非聚簇索引 查询一条数据行的所有列的时候,就需要 回表 去查询其他列的信息了,也就是说需要 两次查询 才完成这个需求。

比方一个表:

1
2
3
4
5
6
CREATE TABLE trbac_user (
id BIGINT auto_increase,
name varchar(50) NOT NULL,
mobile varchar(50) NOT NULL
PRIMARY KEY(id)
)

那我使用 name 来做非聚簇索引,这时候 InnoDB 就有一颗以 name 来查询的 B+树

那我们可以看到,数据行并没有覆盖所有数据,只有 name + id。那比如我们只是查询 name 或者 id 列,就可以直接在这里返回去了。

但是如果我还想要知道 mobile 列的话,这时候就需要在上面那棵树拿到 id,然后再去 id 那棵树查询其他的列信息。

需要从下面的主键的数据页来查询其他数据。

那这里有个什么经验呢…就是我们可以根据经常使用常用的列来建立索引,先拿到 id,然后再去缓存命中,再走数据库~

单列聚簇索引还有个需要注意的点,就是这个列的辨识度要搞,如果这个列只有 MANWOMAN 的话,为这个列建立索引根本就没有什么用处,因为跟全表扫描一样的效率,优化器更偏向于全表扫描,所以切记不要给列值只有寥寥几个的列建立索引,纯属浪费。

5.2 多列非聚簇索引

如果是 name + mobile 联合索引的话,需要注意的点就有点多了:

每个数据页的排序就会变成,先按照 name 进行排序,然后再按照 mobile 进行排序。

5.4 什么时候不会使用索引

1.不满足最左匹配

我想大家都听过 最左匹配原则,就是这个意思,因为我的索引是先根据 name 排序再根据 mobile 进行排序的。那么查询的时候,如果加上 order by 的话,这个排序同样也是派上用场的。

但是如果说查询条件只是写了 WHERE mobile = '13800x1',那上面的索引就完全派不上用场。

怎么说,因为我这个索引是结合了 name 先做排序然后索引的,你只是查询 mobile 的话,就需要遍历整个索引,拿到 id 后还需要回表,这个过程跟直接扫描 聚簇索引 相比,耗费的资源更多,所以,如果一个 name + mobile 的索引,只应用于带有 name 开始的查询条件。因为 name 在前面。这就是 最左匹配原则

2.排序一个列使用升序一个使用降序

比如 WHERE name like 'xxx%' ORDER BY name DESC,mobile ASC,这样的情况只能用到 name 列的索引(也就是所有 mobile 失去了效果),然后再把所有列重新整理,依据 mobile 重新排序。

如果两个都是同一个方向的排序就不会出现这种情况。

3.VARCHAR 匹配前缀查询

WHERE name like '%狗蛋' 这种情况为什么不能使用索引呢,因为你前面未知啊,根据 name 排序的索引查询跟全表扫描有什么区别,还少了 回表 的操作,所以只能全表扫描,取出满足 xxx狗蛋 的数据了。

4.排序用到索引中没有的列

这个理解了 区别在于 非聚簇索引 并没有存放整条记录的所有数据,只是存放了 索引列主键 而已 这句话,就知道为什么了,因为索引中的排序并不适合 SQL 指定的顺序。

5.使用函数修饰值的时候

比如 WHERE LOWER(name) = 'aa',每个列都需要通过函数调用,那就跟全表扫描没什么区别了。


那如果 SQL 语句中,条件出现的顺序没有和多列索引的顺序一致的话,会怎么样:

完全不必担心这个问题,因为 SQL 在执行之前还会交给一个叫做 优化器 的东西进行整理,下面会说到,现在就只要知道他会把 SQL 条件整理成跟索引一样的顺序就好了。

既然多列索引可以帮忙查询条件中满足最左匹配的 SQL 语句,是不是越多越好,答案并不是,我觉得应该根据权重来区分,因为 非聚簇索引 他的索引列并不会像自增 ID 的索引树一样,顺序的插入数据(历史原因导致我们项目使用 UUID 做主键的哭晕在厕所),而是经常的造成 页拆分(也就是上面拆分的动图),所以 非聚簇索引 多了,增删改 需要维护这棵树平衡所做的 整容 就越多,严重的话会影响 增删改 的效率。


5.5 什么时候命中索引

那什么时候用到了索引,自然就是避免上面的情况就可以了,就有这些情况,包括但不局限于:

  1. 条件覆盖了索引的最左列;
  2. 常量查询,WHERE name = 'xxx'
  3. 范围查询,满足 1 的情况下,比如 name > 'AA' 或者 name = 'AA' AND mobile > '13800';
  4. 排序,但是需要用到的排序列是同一个方向进行排序的。
  5. 分组,依然需要满足 1 条件,如果用到了 group by nameInnoDB 会先将相同的 name 放在一起,然后再继续根据其他条件进行运算。

5.6 回表代价

可以这么说,如果回表的代价过高,InnoDB解析器 可能直接决定,不要使用索引,直接全表扫描。

怎么回事,就是 非聚簇 索引他的值都是连在一起的,查询的时候,称为 顺序I/O,而 非聚簇索引 查询出来的 id 并不会连在一起,所以回表去查询 id 数据的时候,称为 随机I/O。而 随机I/O 的效率是很低的,所以当命中索引的数据行总是较多的时候,不如 全表扫描 来的快,所以可能出现 SQL 已经完全命中这个索引但是解析器他就是不使用。

怎么防止这种情况发生,就是让查询索引返回的数量少,比如我们可以 LIMIT 100 去限定只查询 100 条数据,或者我们只要查询索引中出现的列,比如 id 然后再在程序中,根据 id 列表去命中缓存,名不中的再一次性批量查库就可以了。

5.7 文件排序

有时候我们去解析 EXPLAIN 我们的 SQL 语句的时候,会出现 File Sort 类型,这个类型指的是,查询出来的结果不是按照我们 SQL 指定的顺序来的,所以需要在内存中(必要时借助硬盘)来重新对数据进行排序,这个过程就是 File Sort,那如何避免这个类型出现,那就是防止上面不命中索引的情况。

5.8 索引合并

使用等值且命中两个索引的列

比方说我的用户表有 name 索引 和 mobile 索引,那这个条件 WHERE name = 'Weidan' AND mobile = '18588777777' 就可以使用两个索引,然后取 id交集,然后回表查询。但是如果 name > 'Weidan' 这种条件,就不会使用到合并。

但是但是,如果是主键列是范围的话,是可以的,因为他们合并的时候也会操作主键嘛。

不过有没有使用到,还要看成本,如果一个索引命中的数据太多,依然不会使用合并索引。

合并两个索引的列

依然需要满足 列都是等值查询 这个条件,比如 WHERE name = 'Weidan' OR mobile = '18588777777',会合并两个主键再统一回表查询。

范围搜索合并查询

不过如果我们是范围查询,又想使用两个索引怎么办,我们可以把 name > 'Weidan' 以及 monile > '18588777777' 先查询出来 id 然后 union 在一起,再通过 id 去寻找我们想要的数据。

但是还是建议把需要合并的列作为一个 非聚簇 索引来覆盖,效率要高。

七.表连接查询

首先就以最经典的学生、成绩来做示例吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
CREATE TABLE `student` (
`id` int(11) NOT NULL,
`name` varchar(50) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

BEGIN;
INSERT INTO `student` VALUES (1, '狗蛋');
INSERT INTO `student` VALUES (2, '狗剩');
INSERT INTO `student` VALUES (3, '翠花');
COMMIT;

DROP TABLE IF EXISTS `scope`;
CREATE TABLE `scope` (
`id` int(11) NOT NULL,
`subject` varchar(255) DEFAULT NULL,
`scope` int(255) DEFAULT NULL,
`stu_id` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

BEGIN;
INSERT INTO `scope` VALUES (1, '语文', 80, 1);
INSERT INTO `scope` VALUES (2, '数学', 98, 1);
INSERT INTO `scope` VALUES (3, '英语', 59, 2);
INSERT INTO `scope` VALUES (4, '政治', 79, 2);
COMMIT;

7.1 连表查询

那么先列举一个连表很普通的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
SELECT * 
FROM student, scope;

+----+--------+----+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+----+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 2 | 狗剩 | 1 | 语文 | 80 | 1 |
| 3 | 翠花 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 2 | 数学 | 98 | 1 |
| 3 | 翠花 | 2 | 数学 | 98 | 1 |
| 1 | 狗蛋 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 3 | 翠花 | 3 | 英语 | 59 | 2 |
| 1 | 狗蛋 | 4 | 政治 | 79 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
| 3 | 翠花 | 4 | 政治 | 79 | 2 |
+----+--------+----+---------+-------+--------+
12 rows in set (0.00 sec)

可以看到,如果我们不加任何条件,直接就连接两张表的话,取出来的结果就是第一张表所有的数据,去搭配第二张表所有的数据,也就是第一张表有 3 条数据,第二张表有 4 条记录,结果就有 3 ✖ 4 = 12 条记录。这个过程也称为 笛卡尔积。这种情况在日常编程中应该也没人会这么写的吧,仔细想想,如果两张表都是百万级别的数据,笛卡尔积是多恐怖的一件事情。

7.2 内连接

就是由于上面的结果集一般我们都不需要,所以我们会加上一些条件加以限制:

1
2
3
4
5
6
7
8
9
10
11
12
13
SELECT stu.*, scp.*
FROM student stu, scope scp
WHERE stu.id = scp.stu_id;

+----+--------+----+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+----+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
+----+--------+----+---------+-------+--------+
4 rows in set (0.00 sec)

这就是我们想要的结果集,所有参考学生的考试成绩。

InnoDB 是怎样的实现两个表连接的呢,这时候,优化器会根据查询成本确定 驱动表被驱动表

  • 驱动表:先查询的那个表,这里假设是 student 表,所以执行的时候,第一步执行 SELECT * FROM student;

  • 被驱动表:查询出来驱动表所有的数据以后,就会根据驱动表中每一条数据,去执行被驱动表。

    • 比如我现在 student 有三条记录,分别 id1 2 3
    • 然后对三条记录分别执行 SELECT * FROM scope WHERE stu_id = ?
    • 因为 scope 表中不存在 翠花 的成绩,所以 SELECT * FROM scope WHERE stu_id = 3 并没有展示出来,因为这个语句相当于 SELECT * FROM scope WHERE stu_id NOT NULL AND stu_id = 3

那如果上面的语句加上其他条件会怎么样,比如:

1
2
3
4
5
# 查询成绩 90 分以上的学生成绩信息
SELECT stu.*, scp.*
FROM student stu, scope scp
WHERE stu.id = scp.stu_id
AND scp.scope > 90;

那在访问驱动表 student 的时候同上面一样,但是针对驱动表中每一条数据执行被驱动表的时候,就会变成:

SELECT * FROM scope WHERE stu_id NOT NULL AND stu_id = ? AND scope > 90;

一句话就是说,这些条件针对哪个表,在执行那个表的时候就会带上指定的条件。

内连接推荐写法:

1
2
3
SELECT stu.*, scp.*
FROM student stu INNER JOIN scope scp ON stu.id = scp.stu_id
WHERE scp.scope > 90;

效率没有变高,但是可读性变好,在我看来,特定的限定功能就需要放在特定的位置,连接条件在 ON 后面,而查询条件在 WHERE 后面,也可以很好的跟下面的外连接写法统一。

7.3 外连接

外连接的目的是将连接在一起查询的表中,以某个表为主要目的,查询他在另外一个表的所有信息,包括不存在的信息。比如还是上面的例子,翠花她是个坏学生,一个科目都没有参加考试,但是老师如果通过上面的内连接查询来看的话,可能都不知道还有这个人的存在。这就麻烦大了啊,家长上门投诉,我给你钱了,她没参加考试你都不管管的吗。所以这时候,就需要外连接来做这个事情了:

1
2
3
4
5
6
7
8
9
10
11
12
SELECT * 
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id;
+----+--------+------+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+------+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
| 3 | 翠花 | NULL | NULL | NULL | NULL |
+----+--------+------+---------+-------+--------+
5 rows in set (0.00 sec)

这个时候,嗯哼,一抓一个准,没有来考试的,在 scope 表的字段中,显示为 NULL

关于 驱动表被驱动表 的含义已经在上面内连接的内容中指定,那么如果说 内连接 是根据成本来指定 驱动表 的,那 外连接 就是我们来指定驱动表。

驱动表 所有满足我们条件的数据,来查询 被驱动表,如果 被驱动表 不存在 驱动表 中某条数据的关联,显示为 NULL

那么关键点在哪里,就在这个 ON 后面的条件,ON 后面的条件,是指定 被驱动表 中不满足 ON 条件情况下依然要显示的关键。(在内连接中连接条件放 ON 和放 WHERE 效果一样)

如果外连接有几层,比如说三层:

1
2
3
4
SELECT *
FROM teacher t
LEFT JOIN student ON t.id = stu.into
LEFT JOIN scope scp ON stu.id = scope.stu_id;

那么上面这条查询需要怎么走呢,先查询老师所有的学生,得到一张中间表以后,再以这张中间表作为 驱动表 来查询 scope

八.复杂条件查询

MySQL InnoDB 中存在着 SQL解析器,解析器可谓为了查询正确的数据而费尽心思,目的只有一个,就是较快的查询出来正确的数据。其实这里也可以联想到 jvm编译器,为了执行效率比较快,会对字节码进行一些顺序的重新编排,什么 happen before 规则都出来了。

而什么东西查询最快呢,那就是查询常量的时候了。

8.1 整理查询语句

首先就是去除不必要的括号,因为括号如果没有意义存在,那移除了就更有利于优化器的解析了。

比如 WHERE (id = 1) 会被优化成 WHERE id = 1。这个好像也没什么好说的。

复杂一点的 WHERE (id = 1 AND name = 'WEIDAN') AND age = 20 则会被优化成 WHERE id = 1 AND name = 'WEIDAN' AND age = 20

8.2 简化查询条件

WHERE age > 18 AND real_age > age 则直接变成 WHERE age > 18 AND real_age > 18

8.3 删除废话条件

像我之前老师教的,在写 MyBatis Mapper.xml 的时候,因为 WHERE 总是需要动态的条件,所以前面会加一个 WHERE 1 = 1(为啥不用标签,因为标签可读性其实不怎么高)。

那么,如果我写的是 WHERE 1 = 1 AND name LIKE 'Weidan%',那么语句将会被直接优化为 WHERE name LIKE 'Weidan%' 。这么想想好像直接写 WHERE 1 = 1 除了解析器多了点工作,好像也没什么所谓了。

8.4 计算表达式

WHERE age = 9 + 9 那么解析器会直接给你个 WHERE age = 18。不过如果这些计算方式放在了列名上,比如:

1
2
WHERE -age = -18
WHERE to_days(age) = 18

解析器害怕他也优化错了,所以也会放弃优化,一个简单的记忆就是函数发生在等号后面的常量值上,可以优化,但是如果需要依赖列的计算的话,那就放弃优化了。

8.5 必要时转换外连接到内连接

右外连接会被转换为左外连接,毕竟 左外连接 更好确定 驱动表被驱动表

那如果我们在查询 左外连接 的时候,暗示着我得出的结果中,被驱动表不含 NULL 的值,聪明的暖男解析器将会 GET 到这个信息,把 外连接 优化成 内连接

比如上面的例子中:

1
2
3
4
5
6
7
SELECT * 
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id
AND scp.id = 1;

SELECT *
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id;
AND scp.id IS NOT NULL;

那么上面两个语句中是不是就都暗示解析器中,拿到的 scope 中的 id 不能为空。

所以!直接变成这条语句执行:

1
2
3
4
5
6
SELECT * 
FROM student stu INNER JOIN scope scp ON scp.stu_id = stu.id
AND scp.id = 1;

SELECT *
FROM student stu INNER JOIN scope scp ON scp.stu_id = stu.id;;

九.完

InnoDB 会把一个页中的所有数据根据一个很小的基数,比如 4 个记录为一组,去划分分组。那么每个组在数据页中都有对应的槽来记录分组的最大值。

那按照上面的数据,比如我要查询 55

槽0 = 18,槽1 = 54,槽2 = 64,槽3 = 77。

怎么找呢,类似于上面那个二分查找的游戏,槽018,老大说 18-77 之间,槽154,老大说 54-77,直到最后,确定数字是在 槽1槽2 之间,因为 槽2 = 64 大于 55,而 槽1 = 54 小于 55。所以从 槽1 的最大值 54 开始寻找下一条记录,遍历分组里面的数据行(因为这时候需要遍历的长度已经被切到很小了,所以是时候用遍历来做了,因为用其他算法的次数跟遍历差不多),取出 55 这条数据。

那整个查询过程中,需要跟硬盘交互的并不多,我们知道,InnoDB 是通过数据页来进行硬盘与内存的沟通的,查找效率高就意味着,我需要从硬盘取数据的次数越少。

3.3 删除记录

删除记录时,InnoDB 是怎么做的,就是在之前提到的数据行的头信息的 delete_flag 设置为 1,这条记录就会被放入 垃圾链表,当又需要插入新数据的时候,会判断是否合适放在这个位置,再把原来的数据给覆盖掉。

为什么这么做无非就是减少 硬盘IO。如果真的删除的话,需要去同步硬盘,这个过程并不是很重要,所以先标记就好了。

然后这条记录的上一条记录的 next_record 字段将被重新指向当前删除记录的下一条记录。

一.InnoDB表数据

上面聊了这么多这个结构,那个结构的。现在是不是有点好奇,InnoDB 是把数据存在哪里的。答案也很简单,存在一颗 B+ 树种。

InnoDB 中,数据所在的位置没有其他地方,就只有一颗 B+ 树。而我们自己建立的索引,也是一颗 B+ 树。存储完整数据的 B+ 树也叫 聚簇索引,而我们自定义的列索引的 B+ 树,则称为 非聚簇索引

OK,大概明白了这两个东西以后,我们一个一个拆开来说说。

二.B+树是什么

关乎数据结构的东西了,如果觉得很简单,或者说已经学过脑袋有印象,可以跳过这段一个非计算机老男人写的废话。

B+ 树,一切要从一个最简单的例子开始,就是二分查找法。

2.1 二分查找法

我们在聚合的时候,估计都玩过一个游戏,叫做大冒险。确定谁来大冒险,有个方式就是猜数字:由上一轮中的倒霉鬼,来确定一个数字,然后围成一圈的小伙伴们来猜。猜中了,就是下一个倒霉鬼。

那怎么猜的呢,这个倒霉鬼在手机上输入一个数字,盖在地上防止修改。然后从一圈中某个人开始,说一个数字,那么那个站在台上的倒霉蛋就会缩小范围,让下一个人继续猜:

比如数字是69。

那么就有下面的对话:

1
2
3
4
5
6
A: 50
倒霉:50 - 100
B:75
倒霉:50 - 75
C:69
倒霉:恭喜你答对了balabala

这个过程咧,有没有很熟悉,对,就是 二分查找法


那为啥使用二分查找法呢,肯定是因为性能好啊。

那比如我有个 有序 数组:[18, 53, 55, 147, 151, 495, 551, 606, 638, 728]

我一次查询每一个数字,平均次数是 (1+2+3+4+5+6+7+8+9+10)/10 = 5.5次,而使用二分查找法则是 4+3+2+4+3+1+4+3+2+3)/10 = 2.9次。顺序查找最坏的情况也要 10 次,而二分查找最坏是 4 次。这么好的查找效率没有理由不用他。

2.2 二叉查找树

那二分查找和树什么关系,这里就要说到 二叉查找树,根据百度百科的定义:

一棵空树,或者是具有下列性质的二叉树

(1)若左子树不空,则左子树上所有结点的值均小于它的根结点的值;

(2)若右子树不空,则右子树上所有结点的值均大于它的根结点的值;

(3)左、右子树也分别为二叉排序树;

(4)没有键值相等的结点。

那么我现在把上面的数组转换成 二叉查找树,并且给予一个查找的动图:

查询看起来是很方便,但是对数据的增删查改就不是这样的了。有时候,数据插入多了,如果不调整可能会所有都放在了左边或者右边,这样性能就会越来越接近顺序查找。那么,数学家就提出了 平衡二叉树最优二叉树 的定义。

2.3 平衡二叉树

平衡二叉树:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1。这样子就可以让树变得矮胖矮胖的好身材,至于为啥矮胖矮胖才是好身材,因为这样子的话,查询效率才高啊。

比如下面这颗,查找大于 495 的时候,简直就是线性查找的效率了。

image-20200113155115465

那长得丑怎么办,整容(旋转跳跃我闭着眼)啊,不过需要钱(性能)。

那我现在就按照 二叉查找树 那一颗,来插入一个 999,平衡二叉树是在增删改的时候通过旋转来平衡树。

那么有长得帅的读者就要问了,这个跟 B+树 是什么关系。

2.4 B+树

因为 B+树 跟上面的思想差不多,所以,现在就来说说 B+树

B+树 她不是二叉树,但是查询跟 二叉树 差不多,我先放一颗 B+树

这是一颗高度为 2 扇出为 5B+树

那这个怎么实现查找的呢,现在比方说我要查找 606 这条数据:

那么 B+树 是怎么实现新增,然后平衡的呢,跟上面一样发生旋转。旋转的规则如下:

叶子节点是第二层的数据,索引节点是第一层查找时用到的数据。那旋转的情况就是节点页有没有满了:

叶子节点 索引节点 操作
× × 直接插入
× 1. 拆分叶子节点;
2. 中间节点值存入索引节点;
3. 小于中间节点的数据放左叶子,大于或等于放入右叶子。
1. 按照上面情况拆分叶子节点;
2. 然后根据相同的操作再拆分索引节点

第一种就不用演示了,比如新增 1000 直接存入最后边的叶子节点就好了。

接下来我演示第二种,那我就插入 645 这个数字吧。

可以从图中看到,拆分页是多么耗性能的一个东西了,所以我们经常说顺序插入是最好的性能,也要求主键一般呈递增的状态。

但是当左右兄弟节点可能没有满的时候,InnoDB不会急着去拆分数据页,而是会通过旋转的方式来让树达到平衡。这里就不说了。

那如果是第三种情况呢,拆分索引页跟拆分数据页是同样的道理,我就不画出来了,只要知道如果是第三种,性能还要比第二种低一倍,因为不仅拆分数据页,还拆分索引页了。

那如果页中有记录被删除呢,怎么去平衡,这时候就有一个东西叫做 计算因子,如果删除后的页的记录数量小于 计算因子*总页数 的时候,B+ 树会去做 合并操作。那我就继续用这一组数据来做示例。

删除 606 638 以后,B+ 树就会变成下面这个样子:

所以说,B+ 树在发生了修改以后,为了保证查询效率,会对某个一部分的节点进行整容。

三.InnoDB是怎么用B+树

好,结合上一篇的 InnoDB表结构 中的页和数据行的结构,以及刚刚所说的 B+树,现在我们就可以来看看一个表中的数据,是怎样被存储以及怎样被查询的。

从之前我们知道,数据是按照数据页的方式进行存放的,而数据页里面除了记录我们的用户记录行意外,还有一些额外的属性用来表示这一页的类型(所以数据页不仅仅是存放数据的页,也可能是 undo页 等等),而数据行里面也有一些额外的属性来辅助查找。

3.1 查找数据所在的页

之前已经系统的列举过 数据页数据行 的所有属性,现在我就挑在索引中需要用到的属性放在图中就可以了,放上其他的没什么用处,也占用地方。

那我现在就假设,上一节中的叶子节点中的数据(如下图),就是我们一条记录的主键 id,因为我们的记录肯定不止有主键这么简单,所以应该还有其他数据,我暂时使用一些占位的方框做代替。

那么这时候上面在数据在 MySQL 数据页中的结构,应该是这样子的:

叶子节点之间的关系,记录类型中:2 表示最小记录,3 表示最大记录,0 表示用户记录。那如果这棵树没有非叶子节点的话,搜索就是线性的需要一个一个遍历,这时候怎么办咧,B+树 的结构就出现了。

这个结构,就是上面 B+树InnoDB 中数据页的结构了。

3.2 查找真实数据

那么,上面我演示的一个页有三个记录,但其实不止三个记录,我就假设一个页有 16 条记录(其实不止,根据数据的大小)吧。那通过索引只能找到这一页呀,难道要一个一个遍历查询相对应的数据,这时候使用遍历还是有点早了(毕竟遍历并没有那么高效),所以在每个数据页中,又使用了一个槽进行分区。

那现在把镜头切换到一个页中来:

InnoDB 会把一个页中的所有数据根据一个很小的基数,比如 4 个记录为一组,去划分分组。那么每个组在数据页中都有对应的槽来记录分组的最大值。

那按照上面的数据,比如我要查询 55

槽0 = 18,槽1 = 54,槽2 = 64,槽3 = 77。

怎么找呢,类似于上面那个二分查找的游戏,槽018,老大说 18-77 之间,槽154,老大说 54-77,直到最后,确定数字是在 槽1槽2 之间,因为 槽2 = 64 大于 55,而 槽1 = 54 小于 55。所以从 槽1 的最大值 54 开始寻找下一条记录,遍历分组里面的数据行(因为这时候需要遍历的长度已经被切到很小了,所以是时候用遍历来做了,因为用其他算法的次数跟遍历差不多),取出 55 这条数据。

那整个查询过程中,需要跟硬盘交互的并不多,我们知道,InnoDB 是通过数据页来进行硬盘与内存的沟通的,查找效率高就意味着,我需要从硬盘取数据的次数越少。

3.3 删除记录

删除记录时,InnoDB 是怎么做的,就是在之前提到的数据行的头信息的 delete_flag 设置为 1,这条记录就会被放入 垃圾链表,当又需要插入新数据的时候,会判断是否合适放在这个位置,再把原来的数据给覆盖掉。

为什么这么做无非就是减少 硬盘IO。如果真的删除的话,需要去同步硬盘,这个过程并不是很重要,所以先标记就好了。

然后这条记录的上一条记录的 next_record 字段将被重新指向当前删除记录的下一条记录。

四.聚簇索引

那聚簇索引是什么,其实看了上面的图,加点料就可以了:

其实上面的例子我使用的是 id 来做例子,但是每条记录还带有其他所有列的信息(非 NULL)的,如果有 NULL 则会被记录在记录头信息,然后节省了存储数据的空间。

而这种索引,叫做 聚簇索引,意思是什么咧,就是一条记录保存了完整的数据信息,非聚簇则不是。而每条数据都会有一个 primary key,如果没有,InnoDB 会自动加上这个隐藏的列。这也是 MySQL_InnoDB 结构的库保存数据的方式。

所以可以这么说,InnoDB 就是 B+树 构成的。而根节点从建立表开始将会被记录,之后每次查询都会从这个根节点开始查询。

五.非聚簇索引

5.1 简单非聚簇索引

非聚簇索引聚簇索引 有什么区别,区别在于 非聚簇索引 并没有存放整条记录的所有数据,只是存放了 索引列主键 而已。所以当我们通过 非聚簇索引 查询一条数据行的所有列的时候,就需要 回表 去查询其他列的信息了,也就是说需要 两次查询 才完成这个需求。

比方一个表:

1
2
3
4
5
6
CREATE TABLE trbac_user (
id BIGINT auto_increase,
name varchar(50) NOT NULL,
mobile varchar(50) NOT NULL
PRIMARY KEY(id)
)

那我使用 name 来做非聚簇索引,这时候 InnoDB 就有一颗以 name 来查询的 B+树

那我们可以看到,数据行并没有覆盖所有数据,只有 name + id。那比如我们只是查询 name 或者 id 列,就可以直接在这里返回去了。

但是如果我还想要知道 mobile 列的话,这时候就需要在上面那棵树拿到 id,然后再去 id 那棵树查询其他的列信息。

需要从下面的主键的数据页来查询其他数据。

那这里有个什么经验呢…就是我们可以根据经常使用常用的列来建立索引,先拿到 id,然后再去缓存命中,再走数据库~

单列聚簇索引还有个需要注意的点,就是这个列的辨识度要搞,如果这个列只有 MANWOMAN 的话,为这个列建立索引根本就没有什么用处,因为跟全表扫描一样的效率,优化器更偏向于全表扫描,所以切记不要给列值只有寥寥几个的列建立索引,纯属浪费。

5.2 多列非聚簇索引

如果是 name + mobile 联合索引的话,需要注意的点就有点多了:

每个数据页的排序就会变成,先按照 name 进行排序,然后再按照 mobile 进行排序。

5.4 什么时候不会使用索引

1.不满足最左匹配

我想大家都听过 最左匹配原则,就是这个意思,因为我的索引是先根据 name 排序再根据 mobile 进行排序的。那么查询的时候,如果加上 order by 的话,这个排序同样也是派上用场的。

但是如果说查询条件只是写了 WHERE mobile = '13800x1',那上面的索引就完全派不上用场。

怎么说,因为我这个索引是结合了 name 先做排序然后索引的,你只是查询 mobile 的话,就需要遍历整个索引,拿到 id 后还需要回表,这个过程跟直接扫描 聚簇索引 相比,耗费的资源更多,所以,如果一个 name + mobile 的索引,只应用于带有 name 开始的查询条件。因为 name 在前面。这就是 最左匹配原则

2.排序一个列使用升序一个使用降序

比如 WHERE name like 'xxx%' ORDER BY name DESC,mobile ASC,这样的情况只能用到 name 列的索引(也就是所有 mobile 失去了效果),然后再把所有列重新整理,依据 mobile 重新排序。

如果两个都是同一个方向的排序就不会出现这种情况。

3.VARCHAR 匹配前缀查询

WHERE name like '%狗蛋' 这种情况为什么不能使用索引呢,因为你前面未知啊,根据 name 排序的索引查询跟全表扫描有什么区别,还少了 回表 的操作,所以只能全表扫描,取出满足 xxx狗蛋 的数据了。

4.排序用到索引中没有的列

这个理解了 区别在于 非聚簇索引 并没有存放整条记录的所有数据,只是存放了 索引列主键 而已 这句话,就知道为什么了,因为索引中的排序并不适合 SQL 指定的顺序。

5.使用函数修饰值的时候

比如 WHERE LOWER(name) = 'aa',每个列都需要通过函数调用,那就跟全表扫描没什么区别了。


那如果 SQL 语句中,条件出现的顺序没有和多列索引的顺序一致的话,会怎么样:

完全不必担心这个问题,因为 SQL 在执行之前还会交给一个叫做 优化器 的东西进行整理,下面会说到,现在就只要知道他会把 SQL 条件整理成跟索引一样的顺序就好了。

既然多列索引可以帮忙查询条件中满足最左匹配的 SQL 语句,是不是越多越好,答案并不是,我觉得应该根据权重来区分,因为 非聚簇索引 他的索引列并不会像自增 ID 的索引树一样,顺序的插入数据(历史原因导致我们项目使用 UUID 做主键的哭晕在厕所),而是经常的造成 页拆分(也就是上面拆分的动图),所以 非聚簇索引 多了,增删改 需要维护这棵树平衡所做的 整容 就越多,严重的话会影响 增删改 的效率。


5.5 什么时候命中索引

那什么时候用到了索引,自然就是避免上面的情况就可以了,就有这些情况,包括但不局限于:

  1. 条件覆盖了索引的最左列;
  2. 常量查询,WHERE name = 'xxx'
  3. 范围查询,满足 1 的情况下,比如 name > 'AA' 或者 name = 'AA' AND mobile > '13800';
  4. 排序,但是需要用到的排序列是同一个方向进行排序的。
  5. 分组,依然需要满足 1 条件,如果用到了 group by nameInnoDB 会先将相同的 name 放在一起,然后再继续根据其他条件进行运算。

5.6 回表代价

可以这么说,如果回表的代价过高,InnoDB解析器 可能直接决定,不要使用索引,直接全表扫描。

怎么回事,就是 非聚簇 索引他的值都是连在一起的,查询的时候,称为 顺序I/O,而 非聚簇索引 查询出来的 id 并不会连在一起,所以回表去查询 id 数据的时候,称为 随机I/O。而 随机I/O 的效率是很低的,所以当命中索引的数据行总是较多的时候,不如 全表扫描 来的快,所以可能出现 SQL 已经完全命中这个索引但是解析器他就是不使用。

怎么防止这种情况发生,就是让查询索引返回的数量少,比如我们可以 LIMIT 100 去限定只查询 100 条数据,或者我们只要查询索引中出现的列,比如 id 然后再在程序中,根据 id 列表去命中缓存,名不中的再一次性批量查库就可以了。

5.7 文件排序

有时候我们去解析 EXPLAIN 我们的 SQL 语句的时候,会出现 File Sort 类型,这个类型指的是,查询出来的结果不是按照我们 SQL 指定的顺序来的,所以需要在内存中(必要时借助硬盘)来重新对数据进行排序,这个过程就是 File Sort,那如何避免这个类型出现,那就是防止上面不命中索引的情况。

5.8 索引合并

使用等值且命中两个索引的列

比方说我的用户表有 name 索引 和 mobile 索引,那这个条件 WHERE name = 'Weidan' AND mobile = '18588777777' 就可以使用两个索引,然后取 id交集,然后回表查询。但是如果 name > 'Weidan' 这种条件,就不会使用到合并。

但是但是,如果是主键列是范围的话,是可以的,因为他们合并的时候也会操作主键嘛。

不过有没有使用到,还要看成本,如果一个索引命中的数据太多,依然不会使用合并索引。

合并两个索引的列

依然需要满足 列都是等值查询 这个条件,比如 WHERE name = 'Weidan' OR mobile = '18588777777',会合并两个主键再统一回表查询。

范围搜索合并查询

不过如果我们是范围查询,又想使用两个索引怎么办,我们可以把 name > 'Weidan' 以及 monile > '18588777777' 先查询出来 id 然后 union 在一起,再通过 id 去寻找我们想要的数据。

但是还是建议把需要合并的列作为一个 非聚簇 索引来覆盖,效率要高。

七.表连接查询

首先就以最经典的学生、成绩来做示例吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
CREATE TABLE `student` (
`id` int(11) NOT NULL,
`name` varchar(50) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

BEGIN;
INSERT INTO `student` VALUES (1, '狗蛋');
INSERT INTO `student` VALUES (2, '狗剩');
INSERT INTO `student` VALUES (3, '翠花');
COMMIT;

DROP TABLE IF EXISTS `scope`;
CREATE TABLE `scope` (
`id` int(11) NOT NULL,
`subject` varchar(255) DEFAULT NULL,
`scope` int(255) DEFAULT NULL,
`stu_id` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

BEGIN;
INSERT INTO `scope` VALUES (1, '语文', 80, 1);
INSERT INTO `scope` VALUES (2, '数学', 98, 1);
INSERT INTO `scope` VALUES (3, '英语', 59, 2);
INSERT INTO `scope` VALUES (4, '政治', 79, 2);
COMMIT;

7.1 连表查询

那么先列举一个连表很普通的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
SELECT * 
FROM student, scope;

+----+--------+----+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+----+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 2 | 狗剩 | 1 | 语文 | 80 | 1 |
| 3 | 翠花 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 2 | 数学 | 98 | 1 |
| 3 | 翠花 | 2 | 数学 | 98 | 1 |
| 1 | 狗蛋 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 3 | 翠花 | 3 | 英语 | 59 | 2 |
| 1 | 狗蛋 | 4 | 政治 | 79 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
| 3 | 翠花 | 4 | 政治 | 79 | 2 |
+----+--------+----+---------+-------+--------+
12 rows in set (0.00 sec)

可以看到,如果我们不加任何条件,直接就连接两张表的话,取出来的结果就是第一张表所有的数据,去搭配第二张表所有的数据,也就是第一张表有 3 条数据,第二张表有 4 条记录,结果就有 3 ✖ 4 = 12 条记录。这个过程也称为 笛卡尔积。这种情况在日常编程中应该也没人会这么写的吧,仔细想想,如果两张表都是百万级别的数据,笛卡尔积是多恐怖的一件事情。

7.2 内连接

就是由于上面的结果集一般我们都不需要,所以我们会加上一些条件加以限制:

1
2
3
4
5
6
7
8
9
10
11
12
13
SELECT stu.*, scp.*
FROM student stu, scope scp
WHERE stu.id = scp.stu_id;

+----+--------+----+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+----+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
+----+--------+----+---------+-------+--------+
4 rows in set (0.00 sec)

这就是我们想要的结果集,所有参考学生的考试成绩。

InnoDB 是怎样的实现两个表连接的呢,这时候,优化器会根据查询成本确定 驱动表被驱动表

  • 驱动表:先查询的那个表,这里假设是 student 表,所以执行的时候,第一步执行 SELECT * FROM student;

  • 被驱动表:查询出来驱动表所有的数据以后,就会根据驱动表中每一条数据,去执行被驱动表。

    • 比如我现在 student 有三条记录,分别 id1 2 3
    • 然后对三条记录分别执行 SELECT * FROM scope WHERE stu_id = ?
    • 因为 scope 表中不存在 翠花 的成绩,所以 SELECT * FROM scope WHERE stu_id = 3 并没有展示出来,因为这个语句相当于 SELECT * FROM scope WHERE stu_id NOT NULL AND stu_id = 3

那如果上面的语句加上其他条件会怎么样,比如:

1
2
3
4
5
# 查询成绩 90 分以上的学生成绩信息
SELECT stu.*, scp.*
FROM student stu, scope scp
WHERE stu.id = scp.stu_id
AND scp.scope > 90;

那在访问驱动表 student 的时候同上面一样,但是针对驱动表中每一条数据执行被驱动表的时候,就会变成:

SELECT * FROM scope WHERE stu_id NOT NULL AND stu_id = ? AND scope > 90;

一句话就是说,这些条件针对哪个表,在执行那个表的时候就会带上指定的条件。

内连接推荐写法:

1
2
3
SELECT stu.*, scp.*
FROM student stu INNER JOIN scope scp ON stu.id = scp.stu_id
WHERE scp.scope > 90;

效率没有变高,但是可读性变好,在我看来,特定的限定功能就需要放在特定的位置,连接条件在 ON 后面,而查询条件在 WHERE 后面,也可以很好的跟下面的外连接写法统一。

7.3 外连接

外连接的目的是将连接在一起查询的表中,以某个表为主要目的,查询他在另外一个表的所有信息,包括不存在的信息。比如还是上面的例子,翠花她是个坏学生,一个科目都没有参加考试,但是老师如果通过上面的内连接查询来看的话,可能都不知道还有这个人的存在。这就麻烦大了啊,家长上门投诉,我给你钱了,她没参加考试你都不管管的吗。所以这时候,就需要外连接来做这个事情了:

1
2
3
4
5
6
7
8
9
10
11
12
SELECT * 
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id;
+----+--------+------+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+------+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
| 3 | 翠花 | NULL | NULL | NULL | NULL |
+----+--------+------+---------+-------+--------+
5 rows in set (0.00 sec)

这个时候,嗯哼,一抓一个准,没有来考试的,在 scope 表的字段中,显示为 NULL

关于 驱动表被驱动表 的含义已经在上面内连接的内容中指定,那么如果说 内连接 是根据成本来指定 驱动表 的,那 外连接 就是我们来指定驱动表。

驱动表 所有满足我们条件的数据,来查询 被驱动表,如果 被驱动表 不存在 驱动表 中某条数据的关联,显示为 NULL

那么关键点在哪里,就在这个 ON 后面的条件,ON 后面的条件,是指定 被驱动表 中不满足 ON 条件情况下依然要显示的关键。(在内连接中连接条件放 ON 和放 WHERE 效果一样)

如果外连接有几层,比如说三层:

1
2
3
4
SELECT *
FROM teacher t
LEFT JOIN student ON t.id = stu.into
LEFT JOIN scope scp ON stu.id = scope.stu_id;

那么上面这条查询需要怎么走呢,先查询老师所有的学生,得到一张中间表以后,再以这张中间表作为 驱动表 来查询 scope

八.复杂条件查询

MySQL InnoDB 中存在着 SQL解析器,解析器可谓为了查询正确的数据而费尽心思,目的只有一个,就是较快的查询出来正确的数据。其实这里也可以联想到 jvm编译器,为了执行效率比较快,会对字节码进行一些顺序的重新编排,什么 happen before 规则都出来了。

而什么东西查询最快呢,那就是查询常量的时候了。

8.1 整理查询语句

首先就是去除不必要的括号,因为括号如果没有意义存在,那移除了就更有利于优化器的解析了。

比如 WHERE (id = 1) 会被优化成 WHERE id = 1。这个好像也没什么好说的。

复杂一点的 WHERE (id = 1 AND name = 'WEIDAN') AND age = 20 则会被优化成 WHERE id = 1 AND name = 'WEIDAN' AND age = 20

8.2 简化查询条件

WHERE age > 18 AND real_age > age 则直接变成 WHERE age > 18 AND real_age > 18

8.3 删除废话条件

像我之前老师教的,在写 MyBatis Mapper.xml 的时候,因为 WHERE 总是需要动态的条件,所以前面会加一个 WHERE 1 = 1(为啥不用标签,因为标签可读性其实不怎么高)。

那么,如果我写的是 WHERE 1 = 1 AND name LIKE 'Weidan%',那么语句将会被直接优化为 WHERE name LIKE 'Weidan%' 。这么想想好像直接写 WHERE 1 = 1 除了解析器多了点工作,好像也没什么所谓了。

8.4 计算表达式

WHERE age = 9 + 9 那么解析器会直接给你个 WHERE age = 18。不过如果这些计算方式放在了列名上,比如:

1
2
WHERE -age = -18
WHERE to_days(age) = 18

解析器害怕他也优化错了,所以也会放弃优化,一个简单的记忆就是函数发生在等号后面的常量值上,可以优化,但是如果需要依赖列的计算的话,那就放弃优化了。

8.5 必要时转换外连接到内连接

右外连接会被转换为左外连接,毕竟 左外连接 更好确定 驱动表被驱动表

那如果我们在查询 左外连接 的时候,暗示着我得出的结果中,被驱动表不含 NULL 的值,聪明的暖男解析器将会 GET 到这个信息,把 外连接 优化成 内连接

比如上面的例子中:

1
2
3
4
5
6
7
SELECT * 
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id
AND scp.id = 1;

SELECT *
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id;
AND scp.id IS NOT NULL;

那么上面两个语句中是不是就都暗示解析器中,拿到的 scope 中的 id 不能为空。

所以!直接变成这条语句执行:

1
2
3
4
5
6
SELECT * 
FROM student stu INNER JOIN scope scp ON scp.stu_id = stu.id
AND scp.id = 1;

SELECT *
FROM student stu INNER JOIN scope scp ON scp.stu_id = stu.id;;

九.完

四.聚簇索引

那聚簇索引是什么,其实看了上面的图,加点料就可以了:

一.InnoDB表数据

上面聊了这么多这个结构,那个结构的。现在是不是有点好奇,InnoDB 是把数据存在哪里的。答案也很简单,存在一颗 B+ 树种。

InnoDB 中,数据所在的位置没有其他地方,就只有一颗 B+ 树。而我们自己建立的索引,也是一颗 B+ 树。存储完整数据的 B+ 树也叫 聚簇索引,而我们自定义的列索引的 B+ 树,则称为 非聚簇索引

OK,大概明白了这两个东西以后,我们一个一个拆开来说说。

二.B+树是什么

关乎数据结构的东西了,如果觉得很简单,或者说已经学过脑袋有印象,可以跳过这段一个非计算机老男人写的废话。

B+ 树,一切要从一个最简单的例子开始,就是二分查找法。

2.1 二分查找法

我们在聚合的时候,估计都玩过一个游戏,叫做大冒险。确定谁来大冒险,有个方式就是猜数字:由上一轮中的倒霉鬼,来确定一个数字,然后围成一圈的小伙伴们来猜。猜中了,就是下一个倒霉鬼。

那怎么猜的呢,这个倒霉鬼在手机上输入一个数字,盖在地上防止修改。然后从一圈中某个人开始,说一个数字,那么那个站在台上的倒霉蛋就会缩小范围,让下一个人继续猜:

比如数字是69。

那么就有下面的对话:

1
2
3
4
5
6
A: 50
倒霉:50 - 100
B:75
倒霉:50 - 75
C:69
倒霉:恭喜你答对了balabala

这个过程咧,有没有很熟悉,对,就是 二分查找法


那为啥使用二分查找法呢,肯定是因为性能好啊。

那比如我有个 有序 数组:[18, 53, 55, 147, 151, 495, 551, 606, 638, 728]

我一次查询每一个数字,平均次数是 (1+2+3+4+5+6+7+8+9+10)/10 = 5.5次,而使用二分查找法则是 4+3+2+4+3+1+4+3+2+3)/10 = 2.9次。顺序查找最坏的情况也要 10 次,而二分查找最坏是 4 次。这么好的查找效率没有理由不用他。

2.2 二叉查找树

那二分查找和树什么关系,这里就要说到 二叉查找树,根据百度百科的定义:

一棵空树,或者是具有下列性质的二叉树

(1)若左子树不空,则左子树上所有结点的值均小于它的根结点的值;

(2)若右子树不空,则右子树上所有结点的值均大于它的根结点的值;

(3)左、右子树也分别为二叉排序树;

(4)没有键值相等的结点。

那么我现在把上面的数组转换成 二叉查找树,并且给予一个查找的动图:

查询看起来是很方便,但是对数据的增删查改就不是这样的了。有时候,数据插入多了,如果不调整可能会所有都放在了左边或者右边,这样性能就会越来越接近顺序查找。那么,数学家就提出了 平衡二叉树最优二叉树 的定义。

2.3 平衡二叉树

平衡二叉树:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1。这样子就可以让树变得矮胖矮胖的好身材,至于为啥矮胖矮胖才是好身材,因为这样子的话,查询效率才高啊。

比如下面这颗,查找大于 495 的时候,简直就是线性查找的效率了。

image-20200113155115465

那长得丑怎么办,整容(旋转跳跃我闭着眼)啊,不过需要钱(性能)。

那我现在就按照 二叉查找树 那一颗,来插入一个 999,平衡二叉树是在增删改的时候通过旋转来平衡树。

那么有长得帅的读者就要问了,这个跟 B+树 是什么关系。

2.4 B+树

因为 B+树 跟上面的思想差不多,所以,现在就来说说 B+树

B+树 她不是二叉树,但是查询跟 二叉树 差不多,我先放一颗 B+树

这是一颗高度为 2 扇出为 5B+树

那这个怎么实现查找的呢,现在比方说我要查找 606 这条数据:

那么 B+树 是怎么实现新增,然后平衡的呢,跟上面一样发生旋转。旋转的规则如下:

叶子节点是第二层的数据,索引节点是第一层查找时用到的数据。那旋转的情况就是节点页有没有满了:

叶子节点 索引节点 操作
× × 直接插入
× 1. 拆分叶子节点;
2. 中间节点值存入索引节点;
3. 小于中间节点的数据放左叶子,大于或等于放入右叶子。
1. 按照上面情况拆分叶子节点;
2. 然后根据相同的操作再拆分索引节点

第一种就不用演示了,比如新增 1000 直接存入最后边的叶子节点就好了。

接下来我演示第二种,那我就插入 645 这个数字吧。

可以从图中看到,拆分页是多么耗性能的一个东西了,所以我们经常说顺序插入是最好的性能,也要求主键一般呈递增的状态。

但是当左右兄弟节点可能没有满的时候,InnoDB不会急着去拆分数据页,而是会通过旋转的方式来让树达到平衡。这里就不说了。

那如果是第三种情况呢,拆分索引页跟拆分数据页是同样的道理,我就不画出来了,只要知道如果是第三种,性能还要比第二种低一倍,因为不仅拆分数据页,还拆分索引页了。

那如果页中有记录被删除呢,怎么去平衡,这时候就有一个东西叫做 计算因子,如果删除后的页的记录数量小于 计算因子*总页数 的时候,B+ 树会去做 合并操作。那我就继续用这一组数据来做示例。

删除 606 638 以后,B+ 树就会变成下面这个样子:

所以说,B+ 树在发生了修改以后,为了保证查询效率,会对某个一部分的节点进行整容。

三.InnoDB是怎么用B+树

好,结合上一篇的 InnoDB表结构 中的页和数据行的结构,以及刚刚所说的 B+树,现在我们就可以来看看一个表中的数据,是怎样被存储以及怎样被查询的。

从之前我们知道,数据是按照数据页的方式进行存放的,而数据页里面除了记录我们的用户记录行意外,还有一些额外的属性用来表示这一页的类型(所以数据页不仅仅是存放数据的页,也可能是 undo页 等等),而数据行里面也有一些额外的属性来辅助查找。

3.1 查找数据所在的页

之前已经系统的列举过 数据页数据行 的所有属性,现在我就挑在索引中需要用到的属性放在图中就可以了,放上其他的没什么用处,也占用地方。

那我现在就假设,上一节中的叶子节点中的数据(如下图),就是我们一条记录的主键 id,因为我们的记录肯定不止有主键这么简单,所以应该还有其他数据,我暂时使用一些占位的方框做代替。

那么这时候上面在数据在 MySQL 数据页中的结构,应该是这样子的:

叶子节点之间的关系,记录类型中:2 表示最小记录,3 表示最大记录,0 表示用户记录。那如果这棵树没有非叶子节点的话,搜索就是线性的需要一个一个遍历,这时候怎么办咧,B+树 的结构就出现了。

这个结构,就是上面 B+树InnoDB 中数据页的结构了。

3.2 查找真实数据

那么,上面我演示的一个页有三个记录,但其实不止三个记录,我就假设一个页有 16 条记录(其实不止,根据数据的大小)吧。那通过索引只能找到这一页呀,难道要一个一个遍历查询相对应的数据,这时候使用遍历还是有点早了(毕竟遍历并没有那么高效),所以在每个数据页中,又使用了一个槽进行分区。

那现在把镜头切换到一个页中来:

InnoDB 会把一个页中的所有数据根据一个很小的基数,比如 4 个记录为一组,去划分分组。那么每个组在数据页中都有对应的槽来记录分组的最大值。

那按照上面的数据,比如我要查询 55

槽0 = 18,槽1 = 54,槽2 = 64,槽3 = 77。

怎么找呢,类似于上面那个二分查找的游戏,槽018,老大说 18-77 之间,槽154,老大说 54-77,直到最后,确定数字是在 槽1槽2 之间,因为 槽2 = 64 大于 55,而 槽1 = 54 小于 55。所以从 槽1 的最大值 54 开始寻找下一条记录,遍历分组里面的数据行(因为这时候需要遍历的长度已经被切到很小了,所以是时候用遍历来做了,因为用其他算法的次数跟遍历差不多),取出 55 这条数据。

那整个查询过程中,需要跟硬盘交互的并不多,我们知道,InnoDB 是通过数据页来进行硬盘与内存的沟通的,查找效率高就意味着,我需要从硬盘取数据的次数越少。

3.3 删除记录

删除记录时,InnoDB 是怎么做的,就是在之前提到的数据行的头信息的 delete_flag 设置为 1,这条记录就会被放入 垃圾链表,当又需要插入新数据的时候,会判断是否合适放在这个位置,再把原来的数据给覆盖掉。

为什么这么做无非就是减少 硬盘IO。如果真的删除的话,需要去同步硬盘,这个过程并不是很重要,所以先标记就好了。

然后这条记录的上一条记录的 next_record 字段将被重新指向当前删除记录的下一条记录。

四.聚簇索引

那聚簇索引是什么,其实看了上面的图,加点料就可以了:

其实上面的例子我使用的是 id 来做例子,但是每条记录还带有其他所有列的信息(非 NULL)的,如果有 NULL 则会被记录在记录头信息,然后节省了存储数据的空间。

而这种索引,叫做 聚簇索引,意思是什么咧,就是一条记录保存了完整的数据信息,非聚簇则不是。而每条数据都会有一个 primary key,如果没有,InnoDB 会自动加上这个隐藏的列。这也是 MySQL_InnoDB 结构的库保存数据的方式。

所以可以这么说,InnoDB 就是 B+树 构成的。而根节点从建立表开始将会被记录,之后每次查询都会从这个根节点开始查询。

五.非聚簇索引

5.1 简单非聚簇索引

非聚簇索引聚簇索引 有什么区别,区别在于 非聚簇索引 并没有存放整条记录的所有数据,只是存放了 索引列主键 而已。所以当我们通过 非聚簇索引 查询一条数据行的所有列的时候,就需要 回表 去查询其他列的信息了,也就是说需要 两次查询 才完成这个需求。

比方一个表:

1
2
3
4
5
6
CREATE TABLE trbac_user (
id BIGINT auto_increase,
name varchar(50) NOT NULL,
mobile varchar(50) NOT NULL
PRIMARY KEY(id)
)

那我使用 name 来做非聚簇索引,这时候 InnoDB 就有一颗以 name 来查询的 B+树

那我们可以看到,数据行并没有覆盖所有数据,只有 name + id。那比如我们只是查询 name 或者 id 列,就可以直接在这里返回去了。

但是如果我还想要知道 mobile 列的话,这时候就需要在上面那棵树拿到 id,然后再去 id 那棵树查询其他的列信息。

需要从下面的主键的数据页来查询其他数据。

那这里有个什么经验呢…就是我们可以根据经常使用常用的列来建立索引,先拿到 id,然后再去缓存命中,再走数据库~

单列聚簇索引还有个需要注意的点,就是这个列的辨识度要搞,如果这个列只有 MANWOMAN 的话,为这个列建立索引根本就没有什么用处,因为跟全表扫描一样的效率,优化器更偏向于全表扫描,所以切记不要给列值只有寥寥几个的列建立索引,纯属浪费。

5.2 多列非聚簇索引

如果是 name + mobile 联合索引的话,需要注意的点就有点多了:

每个数据页的排序就会变成,先按照 name 进行排序,然后再按照 mobile 进行排序。

5.4 什么时候不会使用索引

1.不满足最左匹配

我想大家都听过 最左匹配原则,就是这个意思,因为我的索引是先根据 name 排序再根据 mobile 进行排序的。那么查询的时候,如果加上 order by 的话,这个排序同样也是派上用场的。

但是如果说查询条件只是写了 WHERE mobile = '13800x1',那上面的索引就完全派不上用场。

怎么说,因为我这个索引是结合了 name 先做排序然后索引的,你只是查询 mobile 的话,就需要遍历整个索引,拿到 id 后还需要回表,这个过程跟直接扫描 聚簇索引 相比,耗费的资源更多,所以,如果一个 name + mobile 的索引,只应用于带有 name 开始的查询条件。因为 name 在前面。这就是 最左匹配原则

2.排序一个列使用升序一个使用降序

比如 WHERE name like 'xxx%' ORDER BY name DESC,mobile ASC,这样的情况只能用到 name 列的索引(也就是所有 mobile 失去了效果),然后再把所有列重新整理,依据 mobile 重新排序。

如果两个都是同一个方向的排序就不会出现这种情况。

3.VARCHAR 匹配前缀查询

WHERE name like '%狗蛋' 这种情况为什么不能使用索引呢,因为你前面未知啊,根据 name 排序的索引查询跟全表扫描有什么区别,还少了 回表 的操作,所以只能全表扫描,取出满足 xxx狗蛋 的数据了。

4.排序用到索引中没有的列

这个理解了 区别在于 非聚簇索引 并没有存放整条记录的所有数据,只是存放了 索引列主键 而已 这句话,就知道为什么了,因为索引中的排序并不适合 SQL 指定的顺序。

5.使用函数修饰值的时候

比如 WHERE LOWER(name) = 'aa',每个列都需要通过函数调用,那就跟全表扫描没什么区别了。


那如果 SQL 语句中,条件出现的顺序没有和多列索引的顺序一致的话,会怎么样:

完全不必担心这个问题,因为 SQL 在执行之前还会交给一个叫做 优化器 的东西进行整理,下面会说到,现在就只要知道他会把 SQL 条件整理成跟索引一样的顺序就好了。

既然多列索引可以帮忙查询条件中满足最左匹配的 SQL 语句,是不是越多越好,答案并不是,我觉得应该根据权重来区分,因为 非聚簇索引 他的索引列并不会像自增 ID 的索引树一样,顺序的插入数据(历史原因导致我们项目使用 UUID 做主键的哭晕在厕所),而是经常的造成 页拆分(也就是上面拆分的动图),所以 非聚簇索引 多了,增删改 需要维护这棵树平衡所做的 整容 就越多,严重的话会影响 增删改 的效率。


5.5 什么时候命中索引

那什么时候用到了索引,自然就是避免上面的情况就可以了,就有这些情况,包括但不局限于:

  1. 条件覆盖了索引的最左列;
  2. 常量查询,WHERE name = 'xxx'
  3. 范围查询,满足 1 的情况下,比如 name > 'AA' 或者 name = 'AA' AND mobile > '13800';
  4. 排序,但是需要用到的排序列是同一个方向进行排序的。
  5. 分组,依然需要满足 1 条件,如果用到了 group by nameInnoDB 会先将相同的 name 放在一起,然后再继续根据其他条件进行运算。

5.6 回表代价

可以这么说,如果回表的代价过高,InnoDB解析器 可能直接决定,不要使用索引,直接全表扫描。

怎么回事,就是 非聚簇 索引他的值都是连在一起的,查询的时候,称为 顺序I/O,而 非聚簇索引 查询出来的 id 并不会连在一起,所以回表去查询 id 数据的时候,称为 随机I/O。而 随机I/O 的效率是很低的,所以当命中索引的数据行总是较多的时候,不如 全表扫描 来的快,所以可能出现 SQL 已经完全命中这个索引但是解析器他就是不使用。

怎么防止这种情况发生,就是让查询索引返回的数量少,比如我们可以 LIMIT 100 去限定只查询 100 条数据,或者我们只要查询索引中出现的列,比如 id 然后再在程序中,根据 id 列表去命中缓存,名不中的再一次性批量查库就可以了。

5.7 文件排序

有时候我们去解析 EXPLAIN 我们的 SQL 语句的时候,会出现 File Sort 类型,这个类型指的是,查询出来的结果不是按照我们 SQL 指定的顺序来的,所以需要在内存中(必要时借助硬盘)来重新对数据进行排序,这个过程就是 File Sort,那如何避免这个类型出现,那就是防止上面不命中索引的情况。

5.8 索引合并

使用等值且命中两个索引的列

比方说我的用户表有 name 索引 和 mobile 索引,那这个条件 WHERE name = 'Weidan' AND mobile = '18588777777' 就可以使用两个索引,然后取 id交集,然后回表查询。但是如果 name > 'Weidan' 这种条件,就不会使用到合并。

但是但是,如果是主键列是范围的话,是可以的,因为他们合并的时候也会操作主键嘛。

不过有没有使用到,还要看成本,如果一个索引命中的数据太多,依然不会使用合并索引。

合并两个索引的列

依然需要满足 列都是等值查询 这个条件,比如 WHERE name = 'Weidan' OR mobile = '18588777777',会合并两个主键再统一回表查询。

范围搜索合并查询

不过如果我们是范围查询,又想使用两个索引怎么办,我们可以把 name > 'Weidan' 以及 monile > '18588777777' 先查询出来 id 然后 union 在一起,再通过 id 去寻找我们想要的数据。

但是还是建议把需要合并的列作为一个 非聚簇 索引来覆盖,效率要高。

七.表连接查询

首先就以最经典的学生、成绩来做示例吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
CREATE TABLE `student` (
`id` int(11) NOT NULL,
`name` varchar(50) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

BEGIN;
INSERT INTO `student` VALUES (1, '狗蛋');
INSERT INTO `student` VALUES (2, '狗剩');
INSERT INTO `student` VALUES (3, '翠花');
COMMIT;

DROP TABLE IF EXISTS `scope`;
CREATE TABLE `scope` (
`id` int(11) NOT NULL,
`subject` varchar(255) DEFAULT NULL,
`scope` int(255) DEFAULT NULL,
`stu_id` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

BEGIN;
INSERT INTO `scope` VALUES (1, '语文', 80, 1);
INSERT INTO `scope` VALUES (2, '数学', 98, 1);
INSERT INTO `scope` VALUES (3, '英语', 59, 2);
INSERT INTO `scope` VALUES (4, '政治', 79, 2);
COMMIT;

7.1 连表查询

那么先列举一个连表很普通的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
SELECT * 
FROM student, scope;

+----+--------+----+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+----+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 2 | 狗剩 | 1 | 语文 | 80 | 1 |
| 3 | 翠花 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 2 | 数学 | 98 | 1 |
| 3 | 翠花 | 2 | 数学 | 98 | 1 |
| 1 | 狗蛋 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 3 | 翠花 | 3 | 英语 | 59 | 2 |
| 1 | 狗蛋 | 4 | 政治 | 79 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
| 3 | 翠花 | 4 | 政治 | 79 | 2 |
+----+--------+----+---------+-------+--------+
12 rows in set (0.00 sec)

可以看到,如果我们不加任何条件,直接就连接两张表的话,取出来的结果就是第一张表所有的数据,去搭配第二张表所有的数据,也就是第一张表有 3 条数据,第二张表有 4 条记录,结果就有 3 ✖ 4 = 12 条记录。这个过程也称为 笛卡尔积。这种情况在日常编程中应该也没人会这么写的吧,仔细想想,如果两张表都是百万级别的数据,笛卡尔积是多恐怖的一件事情。

7.2 内连接

就是由于上面的结果集一般我们都不需要,所以我们会加上一些条件加以限制:

1
2
3
4
5
6
7
8
9
10
11
12
13
SELECT stu.*, scp.*
FROM student stu, scope scp
WHERE stu.id = scp.stu_id;

+----+--------+----+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+----+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
+----+--------+----+---------+-------+--------+
4 rows in set (0.00 sec)

这就是我们想要的结果集,所有参考学生的考试成绩。

InnoDB 是怎样的实现两个表连接的呢,这时候,优化器会根据查询成本确定 驱动表被驱动表

  • 驱动表:先查询的那个表,这里假设是 student 表,所以执行的时候,第一步执行 SELECT * FROM student;

  • 被驱动表:查询出来驱动表所有的数据以后,就会根据驱动表中每一条数据,去执行被驱动表。

    • 比如我现在 student 有三条记录,分别 id1 2 3
    • 然后对三条记录分别执行 SELECT * FROM scope WHERE stu_id = ?
    • 因为 scope 表中不存在 翠花 的成绩,所以 SELECT * FROM scope WHERE stu_id = 3 并没有展示出来,因为这个语句相当于 SELECT * FROM scope WHERE stu_id NOT NULL AND stu_id = 3

那如果上面的语句加上其他条件会怎么样,比如:

1
2
3
4
5
# 查询成绩 90 分以上的学生成绩信息
SELECT stu.*, scp.*
FROM student stu, scope scp
WHERE stu.id = scp.stu_id
AND scp.scope > 90;

那在访问驱动表 student 的时候同上面一样,但是针对驱动表中每一条数据执行被驱动表的时候,就会变成:

SELECT * FROM scope WHERE stu_id NOT NULL AND stu_id = ? AND scope > 90;

一句话就是说,这些条件针对哪个表,在执行那个表的时候就会带上指定的条件。

内连接推荐写法:

1
2
3
SELECT stu.*, scp.*
FROM student stu INNER JOIN scope scp ON stu.id = scp.stu_id
WHERE scp.scope > 90;

效率没有变高,但是可读性变好,在我看来,特定的限定功能就需要放在特定的位置,连接条件在 ON 后面,而查询条件在 WHERE 后面,也可以很好的跟下面的外连接写法统一。

7.3 外连接

外连接的目的是将连接在一起查询的表中,以某个表为主要目的,查询他在另外一个表的所有信息,包括不存在的信息。比如还是上面的例子,翠花她是个坏学生,一个科目都没有参加考试,但是老师如果通过上面的内连接查询来看的话,可能都不知道还有这个人的存在。这就麻烦大了啊,家长上门投诉,我给你钱了,她没参加考试你都不管管的吗。所以这时候,就需要外连接来做这个事情了:

1
2
3
4
5
6
7
8
9
10
11
12
SELECT * 
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id;
+----+--------+------+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+------+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
| 3 | 翠花 | NULL | NULL | NULL | NULL |
+----+--------+------+---------+-------+--------+
5 rows in set (0.00 sec)

这个时候,嗯哼,一抓一个准,没有来考试的,在 scope 表的字段中,显示为 NULL

关于 驱动表被驱动表 的含义已经在上面内连接的内容中指定,那么如果说 内连接 是根据成本来指定 驱动表 的,那 外连接 就是我们来指定驱动表。

驱动表 所有满足我们条件的数据,来查询 被驱动表,如果 被驱动表 不存在 驱动表 中某条数据的关联,显示为 NULL

那么关键点在哪里,就在这个 ON 后面的条件,ON 后面的条件,是指定 被驱动表 中不满足 ON 条件情况下依然要显示的关键。(在内连接中连接条件放 ON 和放 WHERE 效果一样)

如果外连接有几层,比如说三层:

1
2
3
4
SELECT *
FROM teacher t
LEFT JOIN student ON t.id = stu.into
LEFT JOIN scope scp ON stu.id = scope.stu_id;

那么上面这条查询需要怎么走呢,先查询老师所有的学生,得到一张中间表以后,再以这张中间表作为 驱动表 来查询 scope

八.复杂条件查询

MySQL InnoDB 中存在着 SQL解析器,解析器可谓为了查询正确的数据而费尽心思,目的只有一个,就是较快的查询出来正确的数据。其实这里也可以联想到 jvm编译器,为了执行效率比较快,会对字节码进行一些顺序的重新编排,什么 happen before 规则都出来了。

而什么东西查询最快呢,那就是查询常量的时候了。

8.1 整理查询语句

首先就是去除不必要的括号,因为括号如果没有意义存在,那移除了就更有利于优化器的解析了。

比如 WHERE (id = 1) 会被优化成 WHERE id = 1。这个好像也没什么好说的。

复杂一点的 WHERE (id = 1 AND name = 'WEIDAN') AND age = 20 则会被优化成 WHERE id = 1 AND name = 'WEIDAN' AND age = 20

8.2 简化查询条件

WHERE age > 18 AND real_age > age 则直接变成 WHERE age > 18 AND real_age > 18

8.3 删除废话条件

像我之前老师教的,在写 MyBatis Mapper.xml 的时候,因为 WHERE 总是需要动态的条件,所以前面会加一个 WHERE 1 = 1(为啥不用标签,因为标签可读性其实不怎么高)。

那么,如果我写的是 WHERE 1 = 1 AND name LIKE 'Weidan%',那么语句将会被直接优化为 WHERE name LIKE 'Weidan%' 。这么想想好像直接写 WHERE 1 = 1 除了解析器多了点工作,好像也没什么所谓了。

8.4 计算表达式

WHERE age = 9 + 9 那么解析器会直接给你个 WHERE age = 18。不过如果这些计算方式放在了列名上,比如:

1
2
WHERE -age = -18
WHERE to_days(age) = 18

解析器害怕他也优化错了,所以也会放弃优化,一个简单的记忆就是函数发生在等号后面的常量值上,可以优化,但是如果需要依赖列的计算的话,那就放弃优化了。

8.5 必要时转换外连接到内连接

右外连接会被转换为左外连接,毕竟 左外连接 更好确定 驱动表被驱动表

那如果我们在查询 左外连接 的时候,暗示着我得出的结果中,被驱动表不含 NULL 的值,聪明的暖男解析器将会 GET 到这个信息,把 外连接 优化成 内连接

比如上面的例子中:

1
2
3
4
5
6
7
SELECT * 
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id
AND scp.id = 1;

SELECT *
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id;
AND scp.id IS NOT NULL;

那么上面两个语句中是不是就都暗示解析器中,拿到的 scope 中的 id 不能为空。

所以!直接变成这条语句执行:

1
2
3
4
5
6
SELECT * 
FROM student stu INNER JOIN scope scp ON scp.stu_id = stu.id
AND scp.id = 1;

SELECT *
FROM student stu INNER JOIN scope scp ON scp.stu_id = stu.id;;

九.完

其实上面的例子我使用的是 id 来做例子,但是每条记录还带有其他所有列的信息(非 NULL)的,如果有 NULL 则会被记录在记录头信息,然后节省了存储数据的空间。

而这种索引,叫做 聚簇索引,意思是什么咧,就是一条记录保存了完整的数据信息,非聚簇则不是。而每条数据都会有一个 primary key,如果没有,InnoDB 会自动加上这个隐藏的列。这也是 MySQL_InnoDB 结构的库保存数据的方式。

所以可以这么说,InnoDB 就是 B+树 构成的。而根节点从建立表开始将会被记录,之后每次查询都会从这个根节点开始查询。

五.非聚簇索引

5.1 简单非聚簇索引

非聚簇索引聚簇索引 有什么区别,区别在于 非聚簇索引 并没有存放整条记录的所有数据,只是存放了 索引列主键 而已。所以当我们通过 非聚簇索引 查询一条数据行的所有列的时候,就需要 回表 去查询其他列的信息了,也就是说需要 两次查询 才完成这个需求。

比方一个表:

1
2
3
4
5
6
CREATE TABLE trbac_user (
id BIGINT auto_increase,
name varchar(50) NOT NULL,
mobile varchar(50) NOT NULL
PRIMARY KEY(id)
)

那我使用 name 来做非聚簇索引,这时候 InnoDB 就有一颗以 name 来查询的 B+树

一.InnoDB表数据

上面聊了这么多这个结构,那个结构的。现在是不是有点好奇,InnoDB 是把数据存在哪里的。答案也很简单,存在一颗 B+ 树种。

InnoDB 中,数据所在的位置没有其他地方,就只有一颗 B+ 树。而我们自己建立的索引,也是一颗 B+ 树。存储完整数据的 B+ 树也叫 聚簇索引,而我们自定义的列索引的 B+ 树,则称为 非聚簇索引

OK,大概明白了这两个东西以后,我们一个一个拆开来说说。

二.B+树是什么

关乎数据结构的东西了,如果觉得很简单,或者说已经学过脑袋有印象,可以跳过这段一个非计算机老男人写的废话。

B+ 树,一切要从一个最简单的例子开始,就是二分查找法。

2.1 二分查找法

我们在聚合的时候,估计都玩过一个游戏,叫做大冒险。确定谁来大冒险,有个方式就是猜数字:由上一轮中的倒霉鬼,来确定一个数字,然后围成一圈的小伙伴们来猜。猜中了,就是下一个倒霉鬼。

那怎么猜的呢,这个倒霉鬼在手机上输入一个数字,盖在地上防止修改。然后从一圈中某个人开始,说一个数字,那么那个站在台上的倒霉蛋就会缩小范围,让下一个人继续猜:

比如数字是69。

那么就有下面的对话:

1
2
3
4
5
6
A: 50
倒霉:50 - 100
B:75
倒霉:50 - 75
C:69
倒霉:恭喜你答对了balabala

这个过程咧,有没有很熟悉,对,就是 二分查找法


那为啥使用二分查找法呢,肯定是因为性能好啊。

那比如我有个 有序 数组:[18, 53, 55, 147, 151, 495, 551, 606, 638, 728]

我一次查询每一个数字,平均次数是 (1+2+3+4+5+6+7+8+9+10)/10 = 5.5次,而使用二分查找法则是 4+3+2+4+3+1+4+3+2+3)/10 = 2.9次。顺序查找最坏的情况也要 10 次,而二分查找最坏是 4 次。这么好的查找效率没有理由不用他。

2.2 二叉查找树

那二分查找和树什么关系,这里就要说到 二叉查找树,根据百度百科的定义:

一棵空树,或者是具有下列性质的二叉树

(1)若左子树不空,则左子树上所有结点的值均小于它的根结点的值;

(2)若右子树不空,则右子树上所有结点的值均大于它的根结点的值;

(3)左、右子树也分别为二叉排序树;

(4)没有键值相等的结点。

那么我现在把上面的数组转换成 二叉查找树,并且给予一个查找的动图:

查询看起来是很方便,但是对数据的增删查改就不是这样的了。有时候,数据插入多了,如果不调整可能会所有都放在了左边或者右边,这样性能就会越来越接近顺序查找。那么,数学家就提出了 平衡二叉树最优二叉树 的定义。

2.3 平衡二叉树

平衡二叉树:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1。这样子就可以让树变得矮胖矮胖的好身材,至于为啥矮胖矮胖才是好身材,因为这样子的话,查询效率才高啊。

比如下面这颗,查找大于 495 的时候,简直就是线性查找的效率了。

image-20200113155115465

那长得丑怎么办,整容(旋转跳跃我闭着眼)啊,不过需要钱(性能)。

那我现在就按照 二叉查找树 那一颗,来插入一个 999,平衡二叉树是在增删改的时候通过旋转来平衡树。

那么有长得帅的读者就要问了,这个跟 B+树 是什么关系。

2.4 B+树

因为 B+树 跟上面的思想差不多,所以,现在就来说说 B+树

B+树 她不是二叉树,但是查询跟 二叉树 差不多,我先放一颗 B+树

这是一颗高度为 2 扇出为 5B+树

那这个怎么实现查找的呢,现在比方说我要查找 606 这条数据:

那么 B+树 是怎么实现新增,然后平衡的呢,跟上面一样发生旋转。旋转的规则如下:

叶子节点是第二层的数据,索引节点是第一层查找时用到的数据。那旋转的情况就是节点页有没有满了:

叶子节点 索引节点 操作
× × 直接插入
× 1. 拆分叶子节点;
2. 中间节点值存入索引节点;
3. 小于中间节点的数据放左叶子,大于或等于放入右叶子。
1. 按照上面情况拆分叶子节点;
2. 然后根据相同的操作再拆分索引节点

第一种就不用演示了,比如新增 1000 直接存入最后边的叶子节点就好了。

接下来我演示第二种,那我就插入 645 这个数字吧。

可以从图中看到,拆分页是多么耗性能的一个东西了,所以我们经常说顺序插入是最好的性能,也要求主键一般呈递增的状态。

但是当左右兄弟节点可能没有满的时候,InnoDB不会急着去拆分数据页,而是会通过旋转的方式来让树达到平衡。这里就不说了。

那如果是第三种情况呢,拆分索引页跟拆分数据页是同样的道理,我就不画出来了,只要知道如果是第三种,性能还要比第二种低一倍,因为不仅拆分数据页,还拆分索引页了。

那如果页中有记录被删除呢,怎么去平衡,这时候就有一个东西叫做 计算因子,如果删除后的页的记录数量小于 计算因子*总页数 的时候,B+ 树会去做 合并操作。那我就继续用这一组数据来做示例。

删除 606 638 以后,B+ 树就会变成下面这个样子:

所以说,B+ 树在发生了修改以后,为了保证查询效率,会对某个一部分的节点进行整容。

三.InnoDB是怎么用B+树

好,结合上一篇的 InnoDB表结构 中的页和数据行的结构,以及刚刚所说的 B+树,现在我们就可以来看看一个表中的数据,是怎样被存储以及怎样被查询的。

从之前我们知道,数据是按照数据页的方式进行存放的,而数据页里面除了记录我们的用户记录行意外,还有一些额外的属性用来表示这一页的类型(所以数据页不仅仅是存放数据的页,也可能是 undo页 等等),而数据行里面也有一些额外的属性来辅助查找。

3.1 查找数据所在的页

之前已经系统的列举过 数据页数据行 的所有属性,现在我就挑在索引中需要用到的属性放在图中就可以了,放上其他的没什么用处,也占用地方。

那我现在就假设,上一节中的叶子节点中的数据(如下图),就是我们一条记录的主键 id,因为我们的记录肯定不止有主键这么简单,所以应该还有其他数据,我暂时使用一些占位的方框做代替。

那么这时候上面在数据在 MySQL 数据页中的结构,应该是这样子的:

叶子节点之间的关系,记录类型中:2 表示最小记录,3 表示最大记录,0 表示用户记录。那如果这棵树没有非叶子节点的话,搜索就是线性的需要一个一个遍历,这时候怎么办咧,B+树 的结构就出现了。

这个结构,就是上面 B+树InnoDB 中数据页的结构了。

3.2 查找真实数据

那么,上面我演示的一个页有三个记录,但其实不止三个记录,我就假设一个页有 16 条记录(其实不止,根据数据的大小)吧。那通过索引只能找到这一页呀,难道要一个一个遍历查询相对应的数据,这时候使用遍历还是有点早了(毕竟遍历并没有那么高效),所以在每个数据页中,又使用了一个槽进行分区。

那现在把镜头切换到一个页中来:

InnoDB 会把一个页中的所有数据根据一个很小的基数,比如 4 个记录为一组,去划分分组。那么每个组在数据页中都有对应的槽来记录分组的最大值。

那按照上面的数据,比如我要查询 55

槽0 = 18,槽1 = 54,槽2 = 64,槽3 = 77。

怎么找呢,类似于上面那个二分查找的游戏,槽018,老大说 18-77 之间,槽154,老大说 54-77,直到最后,确定数字是在 槽1槽2 之间,因为 槽2 = 64 大于 55,而 槽1 = 54 小于 55。所以从 槽1 的最大值 54 开始寻找下一条记录,遍历分组里面的数据行(因为这时候需要遍历的长度已经被切到很小了,所以是时候用遍历来做了,因为用其他算法的次数跟遍历差不多),取出 55 这条数据。

那整个查询过程中,需要跟硬盘交互的并不多,我们知道,InnoDB 是通过数据页来进行硬盘与内存的沟通的,查找效率高就意味着,我需要从硬盘取数据的次数越少。

3.3 删除记录

删除记录时,InnoDB 是怎么做的,就是在之前提到的数据行的头信息的 delete_flag 设置为 1,这条记录就会被放入 垃圾链表,当又需要插入新数据的时候,会判断是否合适放在这个位置,再把原来的数据给覆盖掉。

为什么这么做无非就是减少 硬盘IO。如果真的删除的话,需要去同步硬盘,这个过程并不是很重要,所以先标记就好了。

然后这条记录的上一条记录的 next_record 字段将被重新指向当前删除记录的下一条记录。

四.聚簇索引

那聚簇索引是什么,其实看了上面的图,加点料就可以了:

其实上面的例子我使用的是 id 来做例子,但是每条记录还带有其他所有列的信息(非 NULL)的,如果有 NULL 则会被记录在记录头信息,然后节省了存储数据的空间。

而这种索引,叫做 聚簇索引,意思是什么咧,就是一条记录保存了完整的数据信息,非聚簇则不是。而每条数据都会有一个 primary key,如果没有,InnoDB 会自动加上这个隐藏的列。这也是 MySQL_InnoDB 结构的库保存数据的方式。

所以可以这么说,InnoDB 就是 B+树 构成的。而根节点从建立表开始将会被记录,之后每次查询都会从这个根节点开始查询。

五.非聚簇索引

5.1 简单非聚簇索引

非聚簇索引聚簇索引 有什么区别,区别在于 非聚簇索引 并没有存放整条记录的所有数据,只是存放了 索引列主键 而已。所以当我们通过 非聚簇索引 查询一条数据行的所有列的时候,就需要 回表 去查询其他列的信息了,也就是说需要 两次查询 才完成这个需求。

比方一个表:

1
2
3
4
5
6
CREATE TABLE trbac_user (
id BIGINT auto_increase,
name varchar(50) NOT NULL,
mobile varchar(50) NOT NULL
PRIMARY KEY(id)
)

那我使用 name 来做非聚簇索引,这时候 InnoDB 就有一颗以 name 来查询的 B+树

那我们可以看到,数据行并没有覆盖所有数据,只有 name + id。那比如我们只是查询 name 或者 id 列,就可以直接在这里返回去了。

但是如果我还想要知道 mobile 列的话,这时候就需要在上面那棵树拿到 id,然后再去 id 那棵树查询其他的列信息。

需要从下面的主键的数据页来查询其他数据。

那这里有个什么经验呢…就是我们可以根据经常使用常用的列来建立索引,先拿到 id,然后再去缓存命中,再走数据库~

单列聚簇索引还有个需要注意的点,就是这个列的辨识度要搞,如果这个列只有 MANWOMAN 的话,为这个列建立索引根本就没有什么用处,因为跟全表扫描一样的效率,优化器更偏向于全表扫描,所以切记不要给列值只有寥寥几个的列建立索引,纯属浪费。

5.2 多列非聚簇索引

如果是 name + mobile 联合索引的话,需要注意的点就有点多了:

每个数据页的排序就会变成,先按照 name 进行排序,然后再按照 mobile 进行排序。

5.4 什么时候不会使用索引

1.不满足最左匹配

我想大家都听过 最左匹配原则,就是这个意思,因为我的索引是先根据 name 排序再根据 mobile 进行排序的。那么查询的时候,如果加上 order by 的话,这个排序同样也是派上用场的。

但是如果说查询条件只是写了 WHERE mobile = '13800x1',那上面的索引就完全派不上用场。

怎么说,因为我这个索引是结合了 name 先做排序然后索引的,你只是查询 mobile 的话,就需要遍历整个索引,拿到 id 后还需要回表,这个过程跟直接扫描 聚簇索引 相比,耗费的资源更多,所以,如果一个 name + mobile 的索引,只应用于带有 name 开始的查询条件。因为 name 在前面。这就是 最左匹配原则

2.排序一个列使用升序一个使用降序

比如 WHERE name like 'xxx%' ORDER BY name DESC,mobile ASC,这样的情况只能用到 name 列的索引(也就是所有 mobile 失去了效果),然后再把所有列重新整理,依据 mobile 重新排序。

如果两个都是同一个方向的排序就不会出现这种情况。

3.VARCHAR 匹配前缀查询

WHERE name like '%狗蛋' 这种情况为什么不能使用索引呢,因为你前面未知啊,根据 name 排序的索引查询跟全表扫描有什么区别,还少了 回表 的操作,所以只能全表扫描,取出满足 xxx狗蛋 的数据了。

4.排序用到索引中没有的列

这个理解了 区别在于 非聚簇索引 并没有存放整条记录的所有数据,只是存放了 索引列主键 而已 这句话,就知道为什么了,因为索引中的排序并不适合 SQL 指定的顺序。

5.使用函数修饰值的时候

比如 WHERE LOWER(name) = 'aa',每个列都需要通过函数调用,那就跟全表扫描没什么区别了。


那如果 SQL 语句中,条件出现的顺序没有和多列索引的顺序一致的话,会怎么样:

完全不必担心这个问题,因为 SQL 在执行之前还会交给一个叫做 优化器 的东西进行整理,下面会说到,现在就只要知道他会把 SQL 条件整理成跟索引一样的顺序就好了。

既然多列索引可以帮忙查询条件中满足最左匹配的 SQL 语句,是不是越多越好,答案并不是,我觉得应该根据权重来区分,因为 非聚簇索引 他的索引列并不会像自增 ID 的索引树一样,顺序的插入数据(历史原因导致我们项目使用 UUID 做主键的哭晕在厕所),而是经常的造成 页拆分(也就是上面拆分的动图),所以 非聚簇索引 多了,增删改 需要维护这棵树平衡所做的 整容 就越多,严重的话会影响 增删改 的效率。


5.5 什么时候命中索引

那什么时候用到了索引,自然就是避免上面的情况就可以了,就有这些情况,包括但不局限于:

  1. 条件覆盖了索引的最左列;
  2. 常量查询,WHERE name = 'xxx'
  3. 范围查询,满足 1 的情况下,比如 name > 'AA' 或者 name = 'AA' AND mobile > '13800';
  4. 排序,但是需要用到的排序列是同一个方向进行排序的。
  5. 分组,依然需要满足 1 条件,如果用到了 group by nameInnoDB 会先将相同的 name 放在一起,然后再继续根据其他条件进行运算。

5.6 回表代价

可以这么说,如果回表的代价过高,InnoDB解析器 可能直接决定,不要使用索引,直接全表扫描。

怎么回事,就是 非聚簇 索引他的值都是连在一起的,查询的时候,称为 顺序I/O,而 非聚簇索引 查询出来的 id 并不会连在一起,所以回表去查询 id 数据的时候,称为 随机I/O。而 随机I/O 的效率是很低的,所以当命中索引的数据行总是较多的时候,不如 全表扫描 来的快,所以可能出现 SQL 已经完全命中这个索引但是解析器他就是不使用。

怎么防止这种情况发生,就是让查询索引返回的数量少,比如我们可以 LIMIT 100 去限定只查询 100 条数据,或者我们只要查询索引中出现的列,比如 id 然后再在程序中,根据 id 列表去命中缓存,名不中的再一次性批量查库就可以了。

5.7 文件排序

有时候我们去解析 EXPLAIN 我们的 SQL 语句的时候,会出现 File Sort 类型,这个类型指的是,查询出来的结果不是按照我们 SQL 指定的顺序来的,所以需要在内存中(必要时借助硬盘)来重新对数据进行排序,这个过程就是 File Sort,那如何避免这个类型出现,那就是防止上面不命中索引的情况。

5.8 索引合并

使用等值且命中两个索引的列

比方说我的用户表有 name 索引 和 mobile 索引,那这个条件 WHERE name = 'Weidan' AND mobile = '18588777777' 就可以使用两个索引,然后取 id交集,然后回表查询。但是如果 name > 'Weidan' 这种条件,就不会使用到合并。

但是但是,如果是主键列是范围的话,是可以的,因为他们合并的时候也会操作主键嘛。

不过有没有使用到,还要看成本,如果一个索引命中的数据太多,依然不会使用合并索引。

合并两个索引的列

依然需要满足 列都是等值查询 这个条件,比如 WHERE name = 'Weidan' OR mobile = '18588777777',会合并两个主键再统一回表查询。

范围搜索合并查询

不过如果我们是范围查询,又想使用两个索引怎么办,我们可以把 name > 'Weidan' 以及 monile > '18588777777' 先查询出来 id 然后 union 在一起,再通过 id 去寻找我们想要的数据。

但是还是建议把需要合并的列作为一个 非聚簇 索引来覆盖,效率要高。

七.表连接查询

首先就以最经典的学生、成绩来做示例吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
CREATE TABLE `student` (
`id` int(11) NOT NULL,
`name` varchar(50) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

BEGIN;
INSERT INTO `student` VALUES (1, '狗蛋');
INSERT INTO `student` VALUES (2, '狗剩');
INSERT INTO `student` VALUES (3, '翠花');
COMMIT;

DROP TABLE IF EXISTS `scope`;
CREATE TABLE `scope` (
`id` int(11) NOT NULL,
`subject` varchar(255) DEFAULT NULL,
`scope` int(255) DEFAULT NULL,
`stu_id` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

BEGIN;
INSERT INTO `scope` VALUES (1, '语文', 80, 1);
INSERT INTO `scope` VALUES (2, '数学', 98, 1);
INSERT INTO `scope` VALUES (3, '英语', 59, 2);
INSERT INTO `scope` VALUES (4, '政治', 79, 2);
COMMIT;

7.1 连表查询

那么先列举一个连表很普通的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
SELECT * 
FROM student, scope;

+----+--------+----+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+----+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 2 | 狗剩 | 1 | 语文 | 80 | 1 |
| 3 | 翠花 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 2 | 数学 | 98 | 1 |
| 3 | 翠花 | 2 | 数学 | 98 | 1 |
| 1 | 狗蛋 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 3 | 翠花 | 3 | 英语 | 59 | 2 |
| 1 | 狗蛋 | 4 | 政治 | 79 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
| 3 | 翠花 | 4 | 政治 | 79 | 2 |
+----+--------+----+---------+-------+--------+
12 rows in set (0.00 sec)

可以看到,如果我们不加任何条件,直接就连接两张表的话,取出来的结果就是第一张表所有的数据,去搭配第二张表所有的数据,也就是第一张表有 3 条数据,第二张表有 4 条记录,结果就有 3 ✖ 4 = 12 条记录。这个过程也称为 笛卡尔积。这种情况在日常编程中应该也没人会这么写的吧,仔细想想,如果两张表都是百万级别的数据,笛卡尔积是多恐怖的一件事情。

7.2 内连接

就是由于上面的结果集一般我们都不需要,所以我们会加上一些条件加以限制:

1
2
3
4
5
6
7
8
9
10
11
12
13
SELECT stu.*, scp.*
FROM student stu, scope scp
WHERE stu.id = scp.stu_id;

+----+--------+----+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+----+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
+----+--------+----+---------+-------+--------+
4 rows in set (0.00 sec)

这就是我们想要的结果集,所有参考学生的考试成绩。

InnoDB 是怎样的实现两个表连接的呢,这时候,优化器会根据查询成本确定 驱动表被驱动表

  • 驱动表:先查询的那个表,这里假设是 student 表,所以执行的时候,第一步执行 SELECT * FROM student;

  • 被驱动表:查询出来驱动表所有的数据以后,就会根据驱动表中每一条数据,去执行被驱动表。

    • 比如我现在 student 有三条记录,分别 id1 2 3
    • 然后对三条记录分别执行 SELECT * FROM scope WHERE stu_id = ?
    • 因为 scope 表中不存在 翠花 的成绩,所以 SELECT * FROM scope WHERE stu_id = 3 并没有展示出来,因为这个语句相当于 SELECT * FROM scope WHERE stu_id NOT NULL AND stu_id = 3

那如果上面的语句加上其他条件会怎么样,比如:

1
2
3
4
5
# 查询成绩 90 分以上的学生成绩信息
SELECT stu.*, scp.*
FROM student stu, scope scp
WHERE stu.id = scp.stu_id
AND scp.scope > 90;

那在访问驱动表 student 的时候同上面一样,但是针对驱动表中每一条数据执行被驱动表的时候,就会变成:

SELECT * FROM scope WHERE stu_id NOT NULL AND stu_id = ? AND scope > 90;

一句话就是说,这些条件针对哪个表,在执行那个表的时候就会带上指定的条件。

内连接推荐写法:

1
2
3
SELECT stu.*, scp.*
FROM student stu INNER JOIN scope scp ON stu.id = scp.stu_id
WHERE scp.scope > 90;

效率没有变高,但是可读性变好,在我看来,特定的限定功能就需要放在特定的位置,连接条件在 ON 后面,而查询条件在 WHERE 后面,也可以很好的跟下面的外连接写法统一。

7.3 外连接

外连接的目的是将连接在一起查询的表中,以某个表为主要目的,查询他在另外一个表的所有信息,包括不存在的信息。比如还是上面的例子,翠花她是个坏学生,一个科目都没有参加考试,但是老师如果通过上面的内连接查询来看的话,可能都不知道还有这个人的存在。这就麻烦大了啊,家长上门投诉,我给你钱了,她没参加考试你都不管管的吗。所以这时候,就需要外连接来做这个事情了:

1
2
3
4
5
6
7
8
9
10
11
12
SELECT * 
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id;
+----+--------+------+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+------+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
| 3 | 翠花 | NULL | NULL | NULL | NULL |
+----+--------+------+---------+-------+--------+
5 rows in set (0.00 sec)

这个时候,嗯哼,一抓一个准,没有来考试的,在 scope 表的字段中,显示为 NULL

关于 驱动表被驱动表 的含义已经在上面内连接的内容中指定,那么如果说 内连接 是根据成本来指定 驱动表 的,那 外连接 就是我们来指定驱动表。

驱动表 所有满足我们条件的数据,来查询 被驱动表,如果 被驱动表 不存在 驱动表 中某条数据的关联,显示为 NULL

那么关键点在哪里,就在这个 ON 后面的条件,ON 后面的条件,是指定 被驱动表 中不满足 ON 条件情况下依然要显示的关键。(在内连接中连接条件放 ON 和放 WHERE 效果一样)

如果外连接有几层,比如说三层:

1
2
3
4
SELECT *
FROM teacher t
LEFT JOIN student ON t.id = stu.into
LEFT JOIN scope scp ON stu.id = scope.stu_id;

那么上面这条查询需要怎么走呢,先查询老师所有的学生,得到一张中间表以后,再以这张中间表作为 驱动表 来查询 scope

八.复杂条件查询

MySQL InnoDB 中存在着 SQL解析器,解析器可谓为了查询正确的数据而费尽心思,目的只有一个,就是较快的查询出来正确的数据。其实这里也可以联想到 jvm编译器,为了执行效率比较快,会对字节码进行一些顺序的重新编排,什么 happen before 规则都出来了。

而什么东西查询最快呢,那就是查询常量的时候了。

8.1 整理查询语句

首先就是去除不必要的括号,因为括号如果没有意义存在,那移除了就更有利于优化器的解析了。

比如 WHERE (id = 1) 会被优化成 WHERE id = 1。这个好像也没什么好说的。

复杂一点的 WHERE (id = 1 AND name = 'WEIDAN') AND age = 20 则会被优化成 WHERE id = 1 AND name = 'WEIDAN' AND age = 20

8.2 简化查询条件

WHERE age > 18 AND real_age > age 则直接变成 WHERE age > 18 AND real_age > 18

8.3 删除废话条件

像我之前老师教的,在写 MyBatis Mapper.xml 的时候,因为 WHERE 总是需要动态的条件,所以前面会加一个 WHERE 1 = 1(为啥不用标签,因为标签可读性其实不怎么高)。

那么,如果我写的是 WHERE 1 = 1 AND name LIKE 'Weidan%',那么语句将会被直接优化为 WHERE name LIKE 'Weidan%' 。这么想想好像直接写 WHERE 1 = 1 除了解析器多了点工作,好像也没什么所谓了。

8.4 计算表达式

WHERE age = 9 + 9 那么解析器会直接给你个 WHERE age = 18。不过如果这些计算方式放在了列名上,比如:

1
2
WHERE -age = -18
WHERE to_days(age) = 18

解析器害怕他也优化错了,所以也会放弃优化,一个简单的记忆就是函数发生在等号后面的常量值上,可以优化,但是如果需要依赖列的计算的话,那就放弃优化了。

8.5 必要时转换外连接到内连接

右外连接会被转换为左外连接,毕竟 左外连接 更好确定 驱动表被驱动表

那如果我们在查询 左外连接 的时候,暗示着我得出的结果中,被驱动表不含 NULL 的值,聪明的暖男解析器将会 GET 到这个信息,把 外连接 优化成 内连接

比如上面的例子中:

1
2
3
4
5
6
7
SELECT * 
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id
AND scp.id = 1;

SELECT *
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id;
AND scp.id IS NOT NULL;

那么上面两个语句中是不是就都暗示解析器中,拿到的 scope 中的 id 不能为空。

所以!直接变成这条语句执行:

1
2
3
4
5
6
SELECT * 
FROM student stu INNER JOIN scope scp ON scp.stu_id = stu.id
AND scp.id = 1;

SELECT *
FROM student stu INNER JOIN scope scp ON scp.stu_id = stu.id;;

九.完

那我们可以看到,数据行并没有覆盖所有数据,只有 name + id。那比如我们只是查询 name 或者 id 列,就可以直接在这里返回去了。

但是如果我还想要知道 mobile 列的话,这时候就需要在上面那棵树拿到 id,然后再去 id 那棵树查询其他的列信息。

需要从下面的主键的数据页来查询其他数据。

一.InnoDB表数据

上面聊了这么多这个结构,那个结构的。现在是不是有点好奇,InnoDB 是把数据存在哪里的。答案也很简单,存在一颗 B+ 树种。

InnoDB 中,数据所在的位置没有其他地方,就只有一颗 B+ 树。而我们自己建立的索引,也是一颗 B+ 树。存储完整数据的 B+ 树也叫 聚簇索引,而我们自定义的列索引的 B+ 树,则称为 非聚簇索引

OK,大概明白了这两个东西以后,我们一个一个拆开来说说。

二.B+树是什么

关乎数据结构的东西了,如果觉得很简单,或者说已经学过脑袋有印象,可以跳过这段一个非计算机老男人写的废话。

B+ 树,一切要从一个最简单的例子开始,就是二分查找法。

2.1 二分查找法

我们在聚合的时候,估计都玩过一个游戏,叫做大冒险。确定谁来大冒险,有个方式就是猜数字:由上一轮中的倒霉鬼,来确定一个数字,然后围成一圈的小伙伴们来猜。猜中了,就是下一个倒霉鬼。

那怎么猜的呢,这个倒霉鬼在手机上输入一个数字,盖在地上防止修改。然后从一圈中某个人开始,说一个数字,那么那个站在台上的倒霉蛋就会缩小范围,让下一个人继续猜:

比如数字是69。

那么就有下面的对话:

1
2
3
4
5
6
A: 50
倒霉:50 - 100
B:75
倒霉:50 - 75
C:69
倒霉:恭喜你答对了balabala

这个过程咧,有没有很熟悉,对,就是 二分查找法


那为啥使用二分查找法呢,肯定是因为性能好啊。

那比如我有个 有序 数组:[18, 53, 55, 147, 151, 495, 551, 606, 638, 728]

我一次查询每一个数字,平均次数是 (1+2+3+4+5+6+7+8+9+10)/10 = 5.5次,而使用二分查找法则是 4+3+2+4+3+1+4+3+2+3)/10 = 2.9次。顺序查找最坏的情况也要 10 次,而二分查找最坏是 4 次。这么好的查找效率没有理由不用他。

2.2 二叉查找树

那二分查找和树什么关系,这里就要说到 二叉查找树,根据百度百科的定义:

一棵空树,或者是具有下列性质的二叉树

(1)若左子树不空,则左子树上所有结点的值均小于它的根结点的值;

(2)若右子树不空,则右子树上所有结点的值均大于它的根结点的值;

(3)左、右子树也分别为二叉排序树;

(4)没有键值相等的结点。

那么我现在把上面的数组转换成 二叉查找树,并且给予一个查找的动图:

查询看起来是很方便,但是对数据的增删查改就不是这样的了。有时候,数据插入多了,如果不调整可能会所有都放在了左边或者右边,这样性能就会越来越接近顺序查找。那么,数学家就提出了 平衡二叉树最优二叉树 的定义。

2.3 平衡二叉树

平衡二叉树:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1。这样子就可以让树变得矮胖矮胖的好身材,至于为啥矮胖矮胖才是好身材,因为这样子的话,查询效率才高啊。

比如下面这颗,查找大于 495 的时候,简直就是线性查找的效率了。

image-20200113155115465

那长得丑怎么办,整容(旋转跳跃我闭着眼)啊,不过需要钱(性能)。

那我现在就按照 二叉查找树 那一颗,来插入一个 999,平衡二叉树是在增删改的时候通过旋转来平衡树。

那么有长得帅的读者就要问了,这个跟 B+树 是什么关系。

2.4 B+树

因为 B+树 跟上面的思想差不多,所以,现在就来说说 B+树

B+树 她不是二叉树,但是查询跟 二叉树 差不多,我先放一颗 B+树

这是一颗高度为 2 扇出为 5B+树

那这个怎么实现查找的呢,现在比方说我要查找 606 这条数据:

那么 B+树 是怎么实现新增,然后平衡的呢,跟上面一样发生旋转。旋转的规则如下:

叶子节点是第二层的数据,索引节点是第一层查找时用到的数据。那旋转的情况就是节点页有没有满了:

叶子节点 索引节点 操作
× × 直接插入
× 1. 拆分叶子节点;
2. 中间节点值存入索引节点;
3. 小于中间节点的数据放左叶子,大于或等于放入右叶子。
1. 按照上面情况拆分叶子节点;
2. 然后根据相同的操作再拆分索引节点

第一种就不用演示了,比如新增 1000 直接存入最后边的叶子节点就好了。

接下来我演示第二种,那我就插入 645 这个数字吧。

可以从图中看到,拆分页是多么耗性能的一个东西了,所以我们经常说顺序插入是最好的性能,也要求主键一般呈递增的状态。

但是当左右兄弟节点可能没有满的时候,InnoDB不会急着去拆分数据页,而是会通过旋转的方式来让树达到平衡。这里就不说了。

那如果是第三种情况呢,拆分索引页跟拆分数据页是同样的道理,我就不画出来了,只要知道如果是第三种,性能还要比第二种低一倍,因为不仅拆分数据页,还拆分索引页了。

那如果页中有记录被删除呢,怎么去平衡,这时候就有一个东西叫做 计算因子,如果删除后的页的记录数量小于 计算因子*总页数 的时候,B+ 树会去做 合并操作。那我就继续用这一组数据来做示例。

删除 606 638 以后,B+ 树就会变成下面这个样子:

所以说,B+ 树在发生了修改以后,为了保证查询效率,会对某个一部分的节点进行整容。

三.InnoDB是怎么用B+树

好,结合上一篇的 InnoDB表结构 中的页和数据行的结构,以及刚刚所说的 B+树,现在我们就可以来看看一个表中的数据,是怎样被存储以及怎样被查询的。

从之前我们知道,数据是按照数据页的方式进行存放的,而数据页里面除了记录我们的用户记录行意外,还有一些额外的属性用来表示这一页的类型(所以数据页不仅仅是存放数据的页,也可能是 undo页 等等),而数据行里面也有一些额外的属性来辅助查找。

3.1 查找数据所在的页

之前已经系统的列举过 数据页数据行 的所有属性,现在我就挑在索引中需要用到的属性放在图中就可以了,放上其他的没什么用处,也占用地方。

那我现在就假设,上一节中的叶子节点中的数据(如下图),就是我们一条记录的主键 id,因为我们的记录肯定不止有主键这么简单,所以应该还有其他数据,我暂时使用一些占位的方框做代替。

那么这时候上面在数据在 MySQL 数据页中的结构,应该是这样子的:

叶子节点之间的关系,记录类型中:2 表示最小记录,3 表示最大记录,0 表示用户记录。那如果这棵树没有非叶子节点的话,搜索就是线性的需要一个一个遍历,这时候怎么办咧,B+树 的结构就出现了。

这个结构,就是上面 B+树InnoDB 中数据页的结构了。

3.2 查找真实数据

那么,上面我演示的一个页有三个记录,但其实不止三个记录,我就假设一个页有 16 条记录(其实不止,根据数据的大小)吧。那通过索引只能找到这一页呀,难道要一个一个遍历查询相对应的数据,这时候使用遍历还是有点早了(毕竟遍历并没有那么高效),所以在每个数据页中,又使用了一个槽进行分区。

那现在把镜头切换到一个页中来:

InnoDB 会把一个页中的所有数据根据一个很小的基数,比如 4 个记录为一组,去划分分组。那么每个组在数据页中都有对应的槽来记录分组的最大值。

那按照上面的数据,比如我要查询 55

槽0 = 18,槽1 = 54,槽2 = 64,槽3 = 77。

怎么找呢,类似于上面那个二分查找的游戏,槽018,老大说 18-77 之间,槽154,老大说 54-77,直到最后,确定数字是在 槽1槽2 之间,因为 槽2 = 64 大于 55,而 槽1 = 54 小于 55。所以从 槽1 的最大值 54 开始寻找下一条记录,遍历分组里面的数据行(因为这时候需要遍历的长度已经被切到很小了,所以是时候用遍历来做了,因为用其他算法的次数跟遍历差不多),取出 55 这条数据。

那整个查询过程中,需要跟硬盘交互的并不多,我们知道,InnoDB 是通过数据页来进行硬盘与内存的沟通的,查找效率高就意味着,我需要从硬盘取数据的次数越少。

3.3 删除记录

删除记录时,InnoDB 是怎么做的,就是在之前提到的数据行的头信息的 delete_flag 设置为 1,这条记录就会被放入 垃圾链表,当又需要插入新数据的时候,会判断是否合适放在这个位置,再把原来的数据给覆盖掉。

为什么这么做无非就是减少 硬盘IO。如果真的删除的话,需要去同步硬盘,这个过程并不是很重要,所以先标记就好了。

然后这条记录的上一条记录的 next_record 字段将被重新指向当前删除记录的下一条记录。

四.聚簇索引

那聚簇索引是什么,其实看了上面的图,加点料就可以了:

其实上面的例子我使用的是 id 来做例子,但是每条记录还带有其他所有列的信息(非 NULL)的,如果有 NULL 则会被记录在记录头信息,然后节省了存储数据的空间。

而这种索引,叫做 聚簇索引,意思是什么咧,就是一条记录保存了完整的数据信息,非聚簇则不是。而每条数据都会有一个 primary key,如果没有,InnoDB 会自动加上这个隐藏的列。这也是 MySQL_InnoDB 结构的库保存数据的方式。

所以可以这么说,InnoDB 就是 B+树 构成的。而根节点从建立表开始将会被记录,之后每次查询都会从这个根节点开始查询。

五.非聚簇索引

5.1 简单非聚簇索引

非聚簇索引聚簇索引 有什么区别,区别在于 非聚簇索引 并没有存放整条记录的所有数据,只是存放了 索引列主键 而已。所以当我们通过 非聚簇索引 查询一条数据行的所有列的时候,就需要 回表 去查询其他列的信息了,也就是说需要 两次查询 才完成这个需求。

比方一个表:

1
2
3
4
5
6
CREATE TABLE trbac_user (
id BIGINT auto_increase,
name varchar(50) NOT NULL,
mobile varchar(50) NOT NULL
PRIMARY KEY(id)
)

那我使用 name 来做非聚簇索引,这时候 InnoDB 就有一颗以 name 来查询的 B+树

那我们可以看到,数据行并没有覆盖所有数据,只有 name + id。那比如我们只是查询 name 或者 id 列,就可以直接在这里返回去了。

但是如果我还想要知道 mobile 列的话,这时候就需要在上面那棵树拿到 id,然后再去 id 那棵树查询其他的列信息。

需要从下面的主键的数据页来查询其他数据。

那这里有个什么经验呢…就是我们可以根据经常使用常用的列来建立索引,先拿到 id,然后再去缓存命中,再走数据库~

单列聚簇索引还有个需要注意的点,就是这个列的辨识度要搞,如果这个列只有 MANWOMAN 的话,为这个列建立索引根本就没有什么用处,因为跟全表扫描一样的效率,优化器更偏向于全表扫描,所以切记不要给列值只有寥寥几个的列建立索引,纯属浪费。

5.2 多列非聚簇索引

如果是 name + mobile 联合索引的话,需要注意的点就有点多了:

每个数据页的排序就会变成,先按照 name 进行排序,然后再按照 mobile 进行排序。

5.4 什么时候不会使用索引

1.不满足最左匹配

我想大家都听过 最左匹配原则,就是这个意思,因为我的索引是先根据 name 排序再根据 mobile 进行排序的。那么查询的时候,如果加上 order by 的话,这个排序同样也是派上用场的。

但是如果说查询条件只是写了 WHERE mobile = '13800x1',那上面的索引就完全派不上用场。

怎么说,因为我这个索引是结合了 name 先做排序然后索引的,你只是查询 mobile 的话,就需要遍历整个索引,拿到 id 后还需要回表,这个过程跟直接扫描 聚簇索引 相比,耗费的资源更多,所以,如果一个 name + mobile 的索引,只应用于带有 name 开始的查询条件。因为 name 在前面。这就是 最左匹配原则

2.排序一个列使用升序一个使用降序

比如 WHERE name like 'xxx%' ORDER BY name DESC,mobile ASC,这样的情况只能用到 name 列的索引(也就是所有 mobile 失去了效果),然后再把所有列重新整理,依据 mobile 重新排序。

如果两个都是同一个方向的排序就不会出现这种情况。

3.VARCHAR 匹配前缀查询

WHERE name like '%狗蛋' 这种情况为什么不能使用索引呢,因为你前面未知啊,根据 name 排序的索引查询跟全表扫描有什么区别,还少了 回表 的操作,所以只能全表扫描,取出满足 xxx狗蛋 的数据了。

4.排序用到索引中没有的列

这个理解了 区别在于 非聚簇索引 并没有存放整条记录的所有数据,只是存放了 索引列主键 而已 这句话,就知道为什么了,因为索引中的排序并不适合 SQL 指定的顺序。

5.使用函数修饰值的时候

比如 WHERE LOWER(name) = 'aa',每个列都需要通过函数调用,那就跟全表扫描没什么区别了。


那如果 SQL 语句中,条件出现的顺序没有和多列索引的顺序一致的话,会怎么样:

完全不必担心这个问题,因为 SQL 在执行之前还会交给一个叫做 优化器 的东西进行整理,下面会说到,现在就只要知道他会把 SQL 条件整理成跟索引一样的顺序就好了。

既然多列索引可以帮忙查询条件中满足最左匹配的 SQL 语句,是不是越多越好,答案并不是,我觉得应该根据权重来区分,因为 非聚簇索引 他的索引列并不会像自增 ID 的索引树一样,顺序的插入数据(历史原因导致我们项目使用 UUID 做主键的哭晕在厕所),而是经常的造成 页拆分(也就是上面拆分的动图),所以 非聚簇索引 多了,增删改 需要维护这棵树平衡所做的 整容 就越多,严重的话会影响 增删改 的效率。


5.5 什么时候命中索引

那什么时候用到了索引,自然就是避免上面的情况就可以了,就有这些情况,包括但不局限于:

  1. 条件覆盖了索引的最左列;
  2. 常量查询,WHERE name = 'xxx'
  3. 范围查询,满足 1 的情况下,比如 name > 'AA' 或者 name = 'AA' AND mobile > '13800';
  4. 排序,但是需要用到的排序列是同一个方向进行排序的。
  5. 分组,依然需要满足 1 条件,如果用到了 group by nameInnoDB 会先将相同的 name 放在一起,然后再继续根据其他条件进行运算。

5.6 回表代价

可以这么说,如果回表的代价过高,InnoDB解析器 可能直接决定,不要使用索引,直接全表扫描。

怎么回事,就是 非聚簇 索引他的值都是连在一起的,查询的时候,称为 顺序I/O,而 非聚簇索引 查询出来的 id 并不会连在一起,所以回表去查询 id 数据的时候,称为 随机I/O。而 随机I/O 的效率是很低的,所以当命中索引的数据行总是较多的时候,不如 全表扫描 来的快,所以可能出现 SQL 已经完全命中这个索引但是解析器他就是不使用。

怎么防止这种情况发生,就是让查询索引返回的数量少,比如我们可以 LIMIT 100 去限定只查询 100 条数据,或者我们只要查询索引中出现的列,比如 id 然后再在程序中,根据 id 列表去命中缓存,名不中的再一次性批量查库就可以了。

5.7 文件排序

有时候我们去解析 EXPLAIN 我们的 SQL 语句的时候,会出现 File Sort 类型,这个类型指的是,查询出来的结果不是按照我们 SQL 指定的顺序来的,所以需要在内存中(必要时借助硬盘)来重新对数据进行排序,这个过程就是 File Sort,那如何避免这个类型出现,那就是防止上面不命中索引的情况。

5.8 索引合并

使用等值且命中两个索引的列

比方说我的用户表有 name 索引 和 mobile 索引,那这个条件 WHERE name = 'Weidan' AND mobile = '18588777777' 就可以使用两个索引,然后取 id交集,然后回表查询。但是如果 name > 'Weidan' 这种条件,就不会使用到合并。

但是但是,如果是主键列是范围的话,是可以的,因为他们合并的时候也会操作主键嘛。

不过有没有使用到,还要看成本,如果一个索引命中的数据太多,依然不会使用合并索引。

合并两个索引的列

依然需要满足 列都是等值查询 这个条件,比如 WHERE name = 'Weidan' OR mobile = '18588777777',会合并两个主键再统一回表查询。

范围搜索合并查询

不过如果我们是范围查询,又想使用两个索引怎么办,我们可以把 name > 'Weidan' 以及 monile > '18588777777' 先查询出来 id 然后 union 在一起,再通过 id 去寻找我们想要的数据。

但是还是建议把需要合并的列作为一个 非聚簇 索引来覆盖,效率要高。

七.表连接查询

首先就以最经典的学生、成绩来做示例吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
CREATE TABLE `student` (
`id` int(11) NOT NULL,
`name` varchar(50) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

BEGIN;
INSERT INTO `student` VALUES (1, '狗蛋');
INSERT INTO `student` VALUES (2, '狗剩');
INSERT INTO `student` VALUES (3, '翠花');
COMMIT;

DROP TABLE IF EXISTS `scope`;
CREATE TABLE `scope` (
`id` int(11) NOT NULL,
`subject` varchar(255) DEFAULT NULL,
`scope` int(255) DEFAULT NULL,
`stu_id` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

BEGIN;
INSERT INTO `scope` VALUES (1, '语文', 80, 1);
INSERT INTO `scope` VALUES (2, '数学', 98, 1);
INSERT INTO `scope` VALUES (3, '英语', 59, 2);
INSERT INTO `scope` VALUES (4, '政治', 79, 2);
COMMIT;

7.1 连表查询

那么先列举一个连表很普通的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
SELECT * 
FROM student, scope;

+----+--------+----+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+----+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 2 | 狗剩 | 1 | 语文 | 80 | 1 |
| 3 | 翠花 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 2 | 数学 | 98 | 1 |
| 3 | 翠花 | 2 | 数学 | 98 | 1 |
| 1 | 狗蛋 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 3 | 翠花 | 3 | 英语 | 59 | 2 |
| 1 | 狗蛋 | 4 | 政治 | 79 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
| 3 | 翠花 | 4 | 政治 | 79 | 2 |
+----+--------+----+---------+-------+--------+
12 rows in set (0.00 sec)

可以看到,如果我们不加任何条件,直接就连接两张表的话,取出来的结果就是第一张表所有的数据,去搭配第二张表所有的数据,也就是第一张表有 3 条数据,第二张表有 4 条记录,结果就有 3 ✖ 4 = 12 条记录。这个过程也称为 笛卡尔积。这种情况在日常编程中应该也没人会这么写的吧,仔细想想,如果两张表都是百万级别的数据,笛卡尔积是多恐怖的一件事情。

7.2 内连接

就是由于上面的结果集一般我们都不需要,所以我们会加上一些条件加以限制:

1
2
3
4
5
6
7
8
9
10
11
12
13
SELECT stu.*, scp.*
FROM student stu, scope scp
WHERE stu.id = scp.stu_id;

+----+--------+----+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+----+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
+----+--------+----+---------+-------+--------+
4 rows in set (0.00 sec)

这就是我们想要的结果集,所有参考学生的考试成绩。

InnoDB 是怎样的实现两个表连接的呢,这时候,优化器会根据查询成本确定 驱动表被驱动表

  • 驱动表:先查询的那个表,这里假设是 student 表,所以执行的时候,第一步执行 SELECT * FROM student;

  • 被驱动表:查询出来驱动表所有的数据以后,就会根据驱动表中每一条数据,去执行被驱动表。

    • 比如我现在 student 有三条记录,分别 id1 2 3
    • 然后对三条记录分别执行 SELECT * FROM scope WHERE stu_id = ?
    • 因为 scope 表中不存在 翠花 的成绩,所以 SELECT * FROM scope WHERE stu_id = 3 并没有展示出来,因为这个语句相当于 SELECT * FROM scope WHERE stu_id NOT NULL AND stu_id = 3

那如果上面的语句加上其他条件会怎么样,比如:

1
2
3
4
5
# 查询成绩 90 分以上的学生成绩信息
SELECT stu.*, scp.*
FROM student stu, scope scp
WHERE stu.id = scp.stu_id
AND scp.scope > 90;

那在访问驱动表 student 的时候同上面一样,但是针对驱动表中每一条数据执行被驱动表的时候,就会变成:

SELECT * FROM scope WHERE stu_id NOT NULL AND stu_id = ? AND scope > 90;

一句话就是说,这些条件针对哪个表,在执行那个表的时候就会带上指定的条件。

内连接推荐写法:

1
2
3
SELECT stu.*, scp.*
FROM student stu INNER JOIN scope scp ON stu.id = scp.stu_id
WHERE scp.scope > 90;

效率没有变高,但是可读性变好,在我看来,特定的限定功能就需要放在特定的位置,连接条件在 ON 后面,而查询条件在 WHERE 后面,也可以很好的跟下面的外连接写法统一。

7.3 外连接

外连接的目的是将连接在一起查询的表中,以某个表为主要目的,查询他在另外一个表的所有信息,包括不存在的信息。比如还是上面的例子,翠花她是个坏学生,一个科目都没有参加考试,但是老师如果通过上面的内连接查询来看的话,可能都不知道还有这个人的存在。这就麻烦大了啊,家长上门投诉,我给你钱了,她没参加考试你都不管管的吗。所以这时候,就需要外连接来做这个事情了:

1
2
3
4
5
6
7
8
9
10
11
12
SELECT * 
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id;
+----+--------+------+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+------+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
| 3 | 翠花 | NULL | NULL | NULL | NULL |
+----+--------+------+---------+-------+--------+
5 rows in set (0.00 sec)

这个时候,嗯哼,一抓一个准,没有来考试的,在 scope 表的字段中,显示为 NULL

关于 驱动表被驱动表 的含义已经在上面内连接的内容中指定,那么如果说 内连接 是根据成本来指定 驱动表 的,那 外连接 就是我们来指定驱动表。

驱动表 所有满足我们条件的数据,来查询 被驱动表,如果 被驱动表 不存在 驱动表 中某条数据的关联,显示为 NULL

那么关键点在哪里,就在这个 ON 后面的条件,ON 后面的条件,是指定 被驱动表 中不满足 ON 条件情况下依然要显示的关键。(在内连接中连接条件放 ON 和放 WHERE 效果一样)

如果外连接有几层,比如说三层:

1
2
3
4
SELECT *
FROM teacher t
LEFT JOIN student ON t.id = stu.into
LEFT JOIN scope scp ON stu.id = scope.stu_id;

那么上面这条查询需要怎么走呢,先查询老师所有的学生,得到一张中间表以后,再以这张中间表作为 驱动表 来查询 scope

八.复杂条件查询

MySQL InnoDB 中存在着 SQL解析器,解析器可谓为了查询正确的数据而费尽心思,目的只有一个,就是较快的查询出来正确的数据。其实这里也可以联想到 jvm编译器,为了执行效率比较快,会对字节码进行一些顺序的重新编排,什么 happen before 规则都出来了。

而什么东西查询最快呢,那就是查询常量的时候了。

8.1 整理查询语句

首先就是去除不必要的括号,因为括号如果没有意义存在,那移除了就更有利于优化器的解析了。

比如 WHERE (id = 1) 会被优化成 WHERE id = 1。这个好像也没什么好说的。

复杂一点的 WHERE (id = 1 AND name = 'WEIDAN') AND age = 20 则会被优化成 WHERE id = 1 AND name = 'WEIDAN' AND age = 20

8.2 简化查询条件

WHERE age > 18 AND real_age > age 则直接变成 WHERE age > 18 AND real_age > 18

8.3 删除废话条件

像我之前老师教的,在写 MyBatis Mapper.xml 的时候,因为 WHERE 总是需要动态的条件,所以前面会加一个 WHERE 1 = 1(为啥不用标签,因为标签可读性其实不怎么高)。

那么,如果我写的是 WHERE 1 = 1 AND name LIKE 'Weidan%',那么语句将会被直接优化为 WHERE name LIKE 'Weidan%' 。这么想想好像直接写 WHERE 1 = 1 除了解析器多了点工作,好像也没什么所谓了。

8.4 计算表达式

WHERE age = 9 + 9 那么解析器会直接给你个 WHERE age = 18。不过如果这些计算方式放在了列名上,比如:

1
2
WHERE -age = -18
WHERE to_days(age) = 18

解析器害怕他也优化错了,所以也会放弃优化,一个简单的记忆就是函数发生在等号后面的常量值上,可以优化,但是如果需要依赖列的计算的话,那就放弃优化了。

8.5 必要时转换外连接到内连接

右外连接会被转换为左外连接,毕竟 左外连接 更好确定 驱动表被驱动表

那如果我们在查询 左外连接 的时候,暗示着我得出的结果中,被驱动表不含 NULL 的值,聪明的暖男解析器将会 GET 到这个信息,把 外连接 优化成 内连接

比如上面的例子中:

1
2
3
4
5
6
7
SELECT * 
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id
AND scp.id = 1;

SELECT *
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id;
AND scp.id IS NOT NULL;

那么上面两个语句中是不是就都暗示解析器中,拿到的 scope 中的 id 不能为空。

所以!直接变成这条语句执行:

1
2
3
4
5
6
SELECT * 
FROM student stu INNER JOIN scope scp ON scp.stu_id = stu.id
AND scp.id = 1;

SELECT *
FROM student stu INNER JOIN scope scp ON scp.stu_id = stu.id;;

九.完

那这里有个什么经验呢…就是我们可以根据经常使用常用的列来建立索引,先拿到 id,然后再去缓存命中,再走数据库~

单列聚簇索引还有个需要注意的点,就是这个列的辨识度要搞,如果这个列只有 MANWOMAN 的话,为这个列建立索引根本就没有什么用处,因为跟全表扫描一样的效率,优化器更偏向于全表扫描,所以切记不要给列值只有寥寥几个的列建立索引,纯属浪费。

5.2 多列非聚簇索引

如果是 name + mobile 联合索引的话,需要注意的点就有点多了:

一.InnoDB表数据

上面聊了这么多这个结构,那个结构的。现在是不是有点好奇,InnoDB 是把数据存在哪里的。答案也很简单,存在一颗 B+ 树种。

InnoDB 中,数据所在的位置没有其他地方,就只有一颗 B+ 树。而我们自己建立的索引,也是一颗 B+ 树。存储完整数据的 B+ 树也叫 聚簇索引,而我们自定义的列索引的 B+ 树,则称为 非聚簇索引

OK,大概明白了这两个东西以后,我们一个一个拆开来说说。

二.B+树是什么

关乎数据结构的东西了,如果觉得很简单,或者说已经学过脑袋有印象,可以跳过这段一个非计算机老男人写的废话。

B+ 树,一切要从一个最简单的例子开始,就是二分查找法。

2.1 二分查找法

我们在聚合的时候,估计都玩过一个游戏,叫做大冒险。确定谁来大冒险,有个方式就是猜数字:由上一轮中的倒霉鬼,来确定一个数字,然后围成一圈的小伙伴们来猜。猜中了,就是下一个倒霉鬼。

那怎么猜的呢,这个倒霉鬼在手机上输入一个数字,盖在地上防止修改。然后从一圈中某个人开始,说一个数字,那么那个站在台上的倒霉蛋就会缩小范围,让下一个人继续猜:

比如数字是69。

那么就有下面的对话:

1
2
3
4
5
6
A: 50
倒霉:50 - 100
B:75
倒霉:50 - 75
C:69
倒霉:恭喜你答对了balabala

这个过程咧,有没有很熟悉,对,就是 二分查找法


那为啥使用二分查找法呢,肯定是因为性能好啊。

那比如我有个 有序 数组:[18, 53, 55, 147, 151, 495, 551, 606, 638, 728]

我一次查询每一个数字,平均次数是 (1+2+3+4+5+6+7+8+9+10)/10 = 5.5次,而使用二分查找法则是 4+3+2+4+3+1+4+3+2+3)/10 = 2.9次。顺序查找最坏的情况也要 10 次,而二分查找最坏是 4 次。这么好的查找效率没有理由不用他。

2.2 二叉查找树

那二分查找和树什么关系,这里就要说到 二叉查找树,根据百度百科的定义:

一棵空树,或者是具有下列性质的二叉树

(1)若左子树不空,则左子树上所有结点的值均小于它的根结点的值;

(2)若右子树不空,则右子树上所有结点的值均大于它的根结点的值;

(3)左、右子树也分别为二叉排序树;

(4)没有键值相等的结点。

那么我现在把上面的数组转换成 二叉查找树,并且给予一个查找的动图:

查询看起来是很方便,但是对数据的增删查改就不是这样的了。有时候,数据插入多了,如果不调整可能会所有都放在了左边或者右边,这样性能就会越来越接近顺序查找。那么,数学家就提出了 平衡二叉树最优二叉树 的定义。

2.3 平衡二叉树

平衡二叉树:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1。这样子就可以让树变得矮胖矮胖的好身材,至于为啥矮胖矮胖才是好身材,因为这样子的话,查询效率才高啊。

比如下面这颗,查找大于 495 的时候,简直就是线性查找的效率了。

image-20200113155115465

那长得丑怎么办,整容(旋转跳跃我闭着眼)啊,不过需要钱(性能)。

那我现在就按照 二叉查找树 那一颗,来插入一个 999,平衡二叉树是在增删改的时候通过旋转来平衡树。

那么有长得帅的读者就要问了,这个跟 B+树 是什么关系。

2.4 B+树

因为 B+树 跟上面的思想差不多,所以,现在就来说说 B+树

B+树 她不是二叉树,但是查询跟 二叉树 差不多,我先放一颗 B+树

这是一颗高度为 2 扇出为 5B+树

那这个怎么实现查找的呢,现在比方说我要查找 606 这条数据:

那么 B+树 是怎么实现新增,然后平衡的呢,跟上面一样发生旋转。旋转的规则如下:

叶子节点是第二层的数据,索引节点是第一层查找时用到的数据。那旋转的情况就是节点页有没有满了:

叶子节点 索引节点 操作
× × 直接插入
× 1. 拆分叶子节点;
2. 中间节点值存入索引节点;
3. 小于中间节点的数据放左叶子,大于或等于放入右叶子。
1. 按照上面情况拆分叶子节点;
2. 然后根据相同的操作再拆分索引节点

第一种就不用演示了,比如新增 1000 直接存入最后边的叶子节点就好了。

接下来我演示第二种,那我就插入 645 这个数字吧。

可以从图中看到,拆分页是多么耗性能的一个东西了,所以我们经常说顺序插入是最好的性能,也要求主键一般呈递增的状态。

但是当左右兄弟节点可能没有满的时候,InnoDB不会急着去拆分数据页,而是会通过旋转的方式来让树达到平衡。这里就不说了。

那如果是第三种情况呢,拆分索引页跟拆分数据页是同样的道理,我就不画出来了,只要知道如果是第三种,性能还要比第二种低一倍,因为不仅拆分数据页,还拆分索引页了。

那如果页中有记录被删除呢,怎么去平衡,这时候就有一个东西叫做 计算因子,如果删除后的页的记录数量小于 计算因子*总页数 的时候,B+ 树会去做 合并操作。那我就继续用这一组数据来做示例。

删除 606 638 以后,B+ 树就会变成下面这个样子:

所以说,B+ 树在发生了修改以后,为了保证查询效率,会对某个一部分的节点进行整容。

三.InnoDB是怎么用B+树

好,结合上一篇的 InnoDB表结构 中的页和数据行的结构,以及刚刚所说的 B+树,现在我们就可以来看看一个表中的数据,是怎样被存储以及怎样被查询的。

从之前我们知道,数据是按照数据页的方式进行存放的,而数据页里面除了记录我们的用户记录行意外,还有一些额外的属性用来表示这一页的类型(所以数据页不仅仅是存放数据的页,也可能是 undo页 等等),而数据行里面也有一些额外的属性来辅助查找。

3.1 查找数据所在的页

之前已经系统的列举过 数据页数据行 的所有属性,现在我就挑在索引中需要用到的属性放在图中就可以了,放上其他的没什么用处,也占用地方。

那我现在就假设,上一节中的叶子节点中的数据(如下图),就是我们一条记录的主键 id,因为我们的记录肯定不止有主键这么简单,所以应该还有其他数据,我暂时使用一些占位的方框做代替。

那么这时候上面在数据在 MySQL 数据页中的结构,应该是这样子的:

叶子节点之间的关系,记录类型中:2 表示最小记录,3 表示最大记录,0 表示用户记录。那如果这棵树没有非叶子节点的话,搜索就是线性的需要一个一个遍历,这时候怎么办咧,B+树 的结构就出现了。

这个结构,就是上面 B+树InnoDB 中数据页的结构了。

3.2 查找真实数据

那么,上面我演示的一个页有三个记录,但其实不止三个记录,我就假设一个页有 16 条记录(其实不止,根据数据的大小)吧。那通过索引只能找到这一页呀,难道要一个一个遍历查询相对应的数据,这时候使用遍历还是有点早了(毕竟遍历并没有那么高效),所以在每个数据页中,又使用了一个槽进行分区。

那现在把镜头切换到一个页中来:

InnoDB 会把一个页中的所有数据根据一个很小的基数,比如 4 个记录为一组,去划分分组。那么每个组在数据页中都有对应的槽来记录分组的最大值。

那按照上面的数据,比如我要查询 55

槽0 = 18,槽1 = 54,槽2 = 64,槽3 = 77。

怎么找呢,类似于上面那个二分查找的游戏,槽018,老大说 18-77 之间,槽154,老大说 54-77,直到最后,确定数字是在 槽1槽2 之间,因为 槽2 = 64 大于 55,而 槽1 = 54 小于 55。所以从 槽1 的最大值 54 开始寻找下一条记录,遍历分组里面的数据行(因为这时候需要遍历的长度已经被切到很小了,所以是时候用遍历来做了,因为用其他算法的次数跟遍历差不多),取出 55 这条数据。

那整个查询过程中,需要跟硬盘交互的并不多,我们知道,InnoDB 是通过数据页来进行硬盘与内存的沟通的,查找效率高就意味着,我需要从硬盘取数据的次数越少。

3.3 删除记录

删除记录时,InnoDB 是怎么做的,就是在之前提到的数据行的头信息的 delete_flag 设置为 1,这条记录就会被放入 垃圾链表,当又需要插入新数据的时候,会判断是否合适放在这个位置,再把原来的数据给覆盖掉。

为什么这么做无非就是减少 硬盘IO。如果真的删除的话,需要去同步硬盘,这个过程并不是很重要,所以先标记就好了。

然后这条记录的上一条记录的 next_record 字段将被重新指向当前删除记录的下一条记录。

四.聚簇索引

那聚簇索引是什么,其实看了上面的图,加点料就可以了:

其实上面的例子我使用的是 id 来做例子,但是每条记录还带有其他所有列的信息(非 NULL)的,如果有 NULL 则会被记录在记录头信息,然后节省了存储数据的空间。

而这种索引,叫做 聚簇索引,意思是什么咧,就是一条记录保存了完整的数据信息,非聚簇则不是。而每条数据都会有一个 primary key,如果没有,InnoDB 会自动加上这个隐藏的列。这也是 MySQL_InnoDB 结构的库保存数据的方式。

所以可以这么说,InnoDB 就是 B+树 构成的。而根节点从建立表开始将会被记录,之后每次查询都会从这个根节点开始查询。

五.非聚簇索引

5.1 简单非聚簇索引

非聚簇索引聚簇索引 有什么区别,区别在于 非聚簇索引 并没有存放整条记录的所有数据,只是存放了 索引列主键 而已。所以当我们通过 非聚簇索引 查询一条数据行的所有列的时候,就需要 回表 去查询其他列的信息了,也就是说需要 两次查询 才完成这个需求。

比方一个表:

1
2
3
4
5
6
CREATE TABLE trbac_user (
id BIGINT auto_increase,
name varchar(50) NOT NULL,
mobile varchar(50) NOT NULL
PRIMARY KEY(id)
)

那我使用 name 来做非聚簇索引,这时候 InnoDB 就有一颗以 name 来查询的 B+树

那我们可以看到,数据行并没有覆盖所有数据,只有 name + id。那比如我们只是查询 name 或者 id 列,就可以直接在这里返回去了。

但是如果我还想要知道 mobile 列的话,这时候就需要在上面那棵树拿到 id,然后再去 id 那棵树查询其他的列信息。

需要从下面的主键的数据页来查询其他数据。

那这里有个什么经验呢…就是我们可以根据经常使用常用的列来建立索引,先拿到 id,然后再去缓存命中,再走数据库~

单列聚簇索引还有个需要注意的点,就是这个列的辨识度要搞,如果这个列只有 MANWOMAN 的话,为这个列建立索引根本就没有什么用处,因为跟全表扫描一样的效率,优化器更偏向于全表扫描,所以切记不要给列值只有寥寥几个的列建立索引,纯属浪费。

5.2 多列非聚簇索引

如果是 name + mobile 联合索引的话,需要注意的点就有点多了:

每个数据页的排序就会变成,先按照 name 进行排序,然后再按照 mobile 进行排序。

5.4 什么时候不会使用索引

1.不满足最左匹配

我想大家都听过 最左匹配原则,就是这个意思,因为我的索引是先根据 name 排序再根据 mobile 进行排序的。那么查询的时候,如果加上 order by 的话,这个排序同样也是派上用场的。

但是如果说查询条件只是写了 WHERE mobile = '13800x1',那上面的索引就完全派不上用场。

怎么说,因为我这个索引是结合了 name 先做排序然后索引的,你只是查询 mobile 的话,就需要遍历整个索引,拿到 id 后还需要回表,这个过程跟直接扫描 聚簇索引 相比,耗费的资源更多,所以,如果一个 name + mobile 的索引,只应用于带有 name 开始的查询条件。因为 name 在前面。这就是 最左匹配原则

2.排序一个列使用升序一个使用降序

比如 WHERE name like 'xxx%' ORDER BY name DESC,mobile ASC,这样的情况只能用到 name 列的索引(也就是所有 mobile 失去了效果),然后再把所有列重新整理,依据 mobile 重新排序。

如果两个都是同一个方向的排序就不会出现这种情况。

3.VARCHAR 匹配前缀查询

WHERE name like '%狗蛋' 这种情况为什么不能使用索引呢,因为你前面未知啊,根据 name 排序的索引查询跟全表扫描有什么区别,还少了 回表 的操作,所以只能全表扫描,取出满足 xxx狗蛋 的数据了。

4.排序用到索引中没有的列

这个理解了 区别在于 非聚簇索引 并没有存放整条记录的所有数据,只是存放了 索引列主键 而已 这句话,就知道为什么了,因为索引中的排序并不适合 SQL 指定的顺序。

5.使用函数修饰值的时候

比如 WHERE LOWER(name) = 'aa',每个列都需要通过函数调用,那就跟全表扫描没什么区别了。


那如果 SQL 语句中,条件出现的顺序没有和多列索引的顺序一致的话,会怎么样:

完全不必担心这个问题,因为 SQL 在执行之前还会交给一个叫做 优化器 的东西进行整理,下面会说到,现在就只要知道他会把 SQL 条件整理成跟索引一样的顺序就好了。

既然多列索引可以帮忙查询条件中满足最左匹配的 SQL 语句,是不是越多越好,答案并不是,我觉得应该根据权重来区分,因为 非聚簇索引 他的索引列并不会像自增 ID 的索引树一样,顺序的插入数据(历史原因导致我们项目使用 UUID 做主键的哭晕在厕所),而是经常的造成 页拆分(也就是上面拆分的动图),所以 非聚簇索引 多了,增删改 需要维护这棵树平衡所做的 整容 就越多,严重的话会影响 增删改 的效率。


5.5 什么时候命中索引

那什么时候用到了索引,自然就是避免上面的情况就可以了,就有这些情况,包括但不局限于:

  1. 条件覆盖了索引的最左列;
  2. 常量查询,WHERE name = 'xxx'
  3. 范围查询,满足 1 的情况下,比如 name > 'AA' 或者 name = 'AA' AND mobile > '13800';
  4. 排序,但是需要用到的排序列是同一个方向进行排序的。
  5. 分组,依然需要满足 1 条件,如果用到了 group by nameInnoDB 会先将相同的 name 放在一起,然后再继续根据其他条件进行运算。

5.6 回表代价

可以这么说,如果回表的代价过高,InnoDB解析器 可能直接决定,不要使用索引,直接全表扫描。

怎么回事,就是 非聚簇 索引他的值都是连在一起的,查询的时候,称为 顺序I/O,而 非聚簇索引 查询出来的 id 并不会连在一起,所以回表去查询 id 数据的时候,称为 随机I/O。而 随机I/O 的效率是很低的,所以当命中索引的数据行总是较多的时候,不如 全表扫描 来的快,所以可能出现 SQL 已经完全命中这个索引但是解析器他就是不使用。

怎么防止这种情况发生,就是让查询索引返回的数量少,比如我们可以 LIMIT 100 去限定只查询 100 条数据,或者我们只要查询索引中出现的列,比如 id 然后再在程序中,根据 id 列表去命中缓存,名不中的再一次性批量查库就可以了。

5.7 文件排序

有时候我们去解析 EXPLAIN 我们的 SQL 语句的时候,会出现 File Sort 类型,这个类型指的是,查询出来的结果不是按照我们 SQL 指定的顺序来的,所以需要在内存中(必要时借助硬盘)来重新对数据进行排序,这个过程就是 File Sort,那如何避免这个类型出现,那就是防止上面不命中索引的情况。

5.8 索引合并

使用等值且命中两个索引的列

比方说我的用户表有 name 索引 和 mobile 索引,那这个条件 WHERE name = 'Weidan' AND mobile = '18588777777' 就可以使用两个索引,然后取 id交集,然后回表查询。但是如果 name > 'Weidan' 这种条件,就不会使用到合并。

但是但是,如果是主键列是范围的话,是可以的,因为他们合并的时候也会操作主键嘛。

不过有没有使用到,还要看成本,如果一个索引命中的数据太多,依然不会使用合并索引。

合并两个索引的列

依然需要满足 列都是等值查询 这个条件,比如 WHERE name = 'Weidan' OR mobile = '18588777777',会合并两个主键再统一回表查询。

范围搜索合并查询

不过如果我们是范围查询,又想使用两个索引怎么办,我们可以把 name > 'Weidan' 以及 monile > '18588777777' 先查询出来 id 然后 union 在一起,再通过 id 去寻找我们想要的数据。

但是还是建议把需要合并的列作为一个 非聚簇 索引来覆盖,效率要高。

七.表连接查询

首先就以最经典的学生、成绩来做示例吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
CREATE TABLE `student` (
`id` int(11) NOT NULL,
`name` varchar(50) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

BEGIN;
INSERT INTO `student` VALUES (1, '狗蛋');
INSERT INTO `student` VALUES (2, '狗剩');
INSERT INTO `student` VALUES (3, '翠花');
COMMIT;

DROP TABLE IF EXISTS `scope`;
CREATE TABLE `scope` (
`id` int(11) NOT NULL,
`subject` varchar(255) DEFAULT NULL,
`scope` int(255) DEFAULT NULL,
`stu_id` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

BEGIN;
INSERT INTO `scope` VALUES (1, '语文', 80, 1);
INSERT INTO `scope` VALUES (2, '数学', 98, 1);
INSERT INTO `scope` VALUES (3, '英语', 59, 2);
INSERT INTO `scope` VALUES (4, '政治', 79, 2);
COMMIT;

7.1 连表查询

那么先列举一个连表很普通的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
SELECT * 
FROM student, scope;

+----+--------+----+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+----+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 2 | 狗剩 | 1 | 语文 | 80 | 1 |
| 3 | 翠花 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 2 | 数学 | 98 | 1 |
| 3 | 翠花 | 2 | 数学 | 98 | 1 |
| 1 | 狗蛋 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 3 | 翠花 | 3 | 英语 | 59 | 2 |
| 1 | 狗蛋 | 4 | 政治 | 79 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
| 3 | 翠花 | 4 | 政治 | 79 | 2 |
+----+--------+----+---------+-------+--------+
12 rows in set (0.00 sec)

可以看到,如果我们不加任何条件,直接就连接两张表的话,取出来的结果就是第一张表所有的数据,去搭配第二张表所有的数据,也就是第一张表有 3 条数据,第二张表有 4 条记录,结果就有 3 ✖ 4 = 12 条记录。这个过程也称为 笛卡尔积。这种情况在日常编程中应该也没人会这么写的吧,仔细想想,如果两张表都是百万级别的数据,笛卡尔积是多恐怖的一件事情。

7.2 内连接

就是由于上面的结果集一般我们都不需要,所以我们会加上一些条件加以限制:

1
2
3
4
5
6
7
8
9
10
11
12
13
SELECT stu.*, scp.*
FROM student stu, scope scp
WHERE stu.id = scp.stu_id;

+----+--------+----+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+----+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
+----+--------+----+---------+-------+--------+
4 rows in set (0.00 sec)

这就是我们想要的结果集,所有参考学生的考试成绩。

InnoDB 是怎样的实现两个表连接的呢,这时候,优化器会根据查询成本确定 驱动表被驱动表

  • 驱动表:先查询的那个表,这里假设是 student 表,所以执行的时候,第一步执行 SELECT * FROM student;

  • 被驱动表:查询出来驱动表所有的数据以后,就会根据驱动表中每一条数据,去执行被驱动表。

    • 比如我现在 student 有三条记录,分别 id1 2 3
    • 然后对三条记录分别执行 SELECT * FROM scope WHERE stu_id = ?
    • 因为 scope 表中不存在 翠花 的成绩,所以 SELECT * FROM scope WHERE stu_id = 3 并没有展示出来,因为这个语句相当于 SELECT * FROM scope WHERE stu_id NOT NULL AND stu_id = 3

那如果上面的语句加上其他条件会怎么样,比如:

1
2
3
4
5
# 查询成绩 90 分以上的学生成绩信息
SELECT stu.*, scp.*
FROM student stu, scope scp
WHERE stu.id = scp.stu_id
AND scp.scope > 90;

那在访问驱动表 student 的时候同上面一样,但是针对驱动表中每一条数据执行被驱动表的时候,就会变成:

SELECT * FROM scope WHERE stu_id NOT NULL AND stu_id = ? AND scope > 90;

一句话就是说,这些条件针对哪个表,在执行那个表的时候就会带上指定的条件。

内连接推荐写法:

1
2
3
SELECT stu.*, scp.*
FROM student stu INNER JOIN scope scp ON stu.id = scp.stu_id
WHERE scp.scope > 90;

效率没有变高,但是可读性变好,在我看来,特定的限定功能就需要放在特定的位置,连接条件在 ON 后面,而查询条件在 WHERE 后面,也可以很好的跟下面的外连接写法统一。

7.3 外连接

外连接的目的是将连接在一起查询的表中,以某个表为主要目的,查询他在另外一个表的所有信息,包括不存在的信息。比如还是上面的例子,翠花她是个坏学生,一个科目都没有参加考试,但是老师如果通过上面的内连接查询来看的话,可能都不知道还有这个人的存在。这就麻烦大了啊,家长上门投诉,我给你钱了,她没参加考试你都不管管的吗。所以这时候,就需要外连接来做这个事情了:

1
2
3
4
5
6
7
8
9
10
11
12
SELECT * 
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id;
+----+--------+------+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+------+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
| 3 | 翠花 | NULL | NULL | NULL | NULL |
+----+--------+------+---------+-------+--------+
5 rows in set (0.00 sec)

这个时候,嗯哼,一抓一个准,没有来考试的,在 scope 表的字段中,显示为 NULL

关于 驱动表被驱动表 的含义已经在上面内连接的内容中指定,那么如果说 内连接 是根据成本来指定 驱动表 的,那 外连接 就是我们来指定驱动表。

驱动表 所有满足我们条件的数据,来查询 被驱动表,如果 被驱动表 不存在 驱动表 中某条数据的关联,显示为 NULL

那么关键点在哪里,就在这个 ON 后面的条件,ON 后面的条件,是指定 被驱动表 中不满足 ON 条件情况下依然要显示的关键。(在内连接中连接条件放 ON 和放 WHERE 效果一样)

如果外连接有几层,比如说三层:

1
2
3
4
SELECT *
FROM teacher t
LEFT JOIN student ON t.id = stu.into
LEFT JOIN scope scp ON stu.id = scope.stu_id;

那么上面这条查询需要怎么走呢,先查询老师所有的学生,得到一张中间表以后,再以这张中间表作为 驱动表 来查询 scope

八.复杂条件查询

MySQL InnoDB 中存在着 SQL解析器,解析器可谓为了查询正确的数据而费尽心思,目的只有一个,就是较快的查询出来正确的数据。其实这里也可以联想到 jvm编译器,为了执行效率比较快,会对字节码进行一些顺序的重新编排,什么 happen before 规则都出来了。

而什么东西查询最快呢,那就是查询常量的时候了。

8.1 整理查询语句

首先就是去除不必要的括号,因为括号如果没有意义存在,那移除了就更有利于优化器的解析了。

比如 WHERE (id = 1) 会被优化成 WHERE id = 1。这个好像也没什么好说的。

复杂一点的 WHERE (id = 1 AND name = 'WEIDAN') AND age = 20 则会被优化成 WHERE id = 1 AND name = 'WEIDAN' AND age = 20

8.2 简化查询条件

WHERE age > 18 AND real_age > age 则直接变成 WHERE age > 18 AND real_age > 18

8.3 删除废话条件

像我之前老师教的,在写 MyBatis Mapper.xml 的时候,因为 WHERE 总是需要动态的条件,所以前面会加一个 WHERE 1 = 1(为啥不用标签,因为标签可读性其实不怎么高)。

那么,如果我写的是 WHERE 1 = 1 AND name LIKE 'Weidan%',那么语句将会被直接优化为 WHERE name LIKE 'Weidan%' 。这么想想好像直接写 WHERE 1 = 1 除了解析器多了点工作,好像也没什么所谓了。

8.4 计算表达式

WHERE age = 9 + 9 那么解析器会直接给你个 WHERE age = 18。不过如果这些计算方式放在了列名上,比如:

1
2
WHERE -age = -18
WHERE to_days(age) = 18

解析器害怕他也优化错了,所以也会放弃优化,一个简单的记忆就是函数发生在等号后面的常量值上,可以优化,但是如果需要依赖列的计算的话,那就放弃优化了。

8.5 必要时转换外连接到内连接

右外连接会被转换为左外连接,毕竟 左外连接 更好确定 驱动表被驱动表

那如果我们在查询 左外连接 的时候,暗示着我得出的结果中,被驱动表不含 NULL 的值,聪明的暖男解析器将会 GET 到这个信息,把 外连接 优化成 内连接

比如上面的例子中:

1
2
3
4
5
6
7
SELECT * 
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id
AND scp.id = 1;

SELECT *
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id;
AND scp.id IS NOT NULL;

那么上面两个语句中是不是就都暗示解析器中,拿到的 scope 中的 id 不能为空。

所以!直接变成这条语句执行:

1
2
3
4
5
6
SELECT * 
FROM student stu INNER JOIN scope scp ON scp.stu_id = stu.id
AND scp.id = 1;

SELECT *
FROM student stu INNER JOIN scope scp ON scp.stu_id = stu.id;;

九.完

每个数据页的排序就会变成,先按照 name 进行排序,然后再按照 mobile 进行排序。

5.4 什么时候不会使用索引

1.不满足最左匹配

我想大家都听过 最左匹配原则,就是这个意思,因为我的索引是先根据 name 排序再根据 mobile 进行排序的。那么查询的时候,如果加上 order by 的话,这个排序同样也是派上用场的。

但是如果说查询条件只是写了 WHERE mobile = '13800x1',那上面的索引就完全派不上用场。

怎么说,因为我这个索引是结合了 name 先做排序然后索引的,你只是查询 mobile 的话,就需要遍历整个索引,拿到 id 后还需要回表,这个过程跟直接扫描 聚簇索引 相比,耗费的资源更多,所以,如果一个 name + mobile 的索引,只应用于带有 name 开始的查询条件。因为 name 在前面。这就是 最左匹配原则

2.排序一个列使用升序一个使用降序

比如 WHERE name like 'xxx%' ORDER BY name DESC,mobile ASC,这样的情况只能用到 name 列的索引(也就是所有 mobile 失去了效果),然后再把所有列重新整理,依据 mobile 重新排序。

如果两个都是同一个方向的排序就不会出现这种情况。

3.VARCHAR 匹配前缀查询

WHERE name like '%狗蛋' 这种情况为什么不能使用索引呢,因为你前面未知啊,根据 name 排序的索引查询跟全表扫描有什么区别,还少了 回表 的操作,所以只能全表扫描,取出满足 xxx狗蛋 的数据了。

4.排序用到索引中没有的列

这个理解了 区别在于 非聚簇索引 并没有存放整条记录的所有数据,只是存放了 索引列主键 而已 这句话,就知道为什么了,因为索引中的排序并不适合 SQL 指定的顺序。

5.使用函数修饰值的时候

比如 WHERE LOWER(name) = 'aa',每个列都需要通过函数调用,那就跟全表扫描没什么区别了。


那如果 SQL 语句中,条件出现的顺序没有和多列索引的顺序一致的话,会怎么样:

完全不必担心这个问题,因为 SQL 在执行之前还会交给一个叫做 优化器 的东西进行整理,下面会说到,现在就只要知道他会把 SQL 条件整理成跟索引一样的顺序就好了。

既然多列索引可以帮忙查询条件中满足最左匹配的 SQL 语句,是不是越多越好,答案并不是,我觉得应该根据权重来区分,因为 非聚簇索引 他的索引列并不会像自增 ID 的索引树一样,顺序的插入数据(历史原因导致我们项目使用 UUID 做主键的哭晕在厕所),而是经常的造成 页拆分(也就是上面拆分的动图),所以 非聚簇索引 多了,增删改 需要维护这棵树平衡所做的 整容 就越多,严重的话会影响 增删改 的效率。


5.5 什么时候命中索引

那什么时候用到了索引,自然就是避免上面的情况就可以了,就有这些情况,包括但不局限于:

  1. 条件覆盖了索引的最左列;
  2. 常量查询,WHERE name = 'xxx'
  3. 范围查询,满足 1 的情况下,比如 name > 'AA' 或者 name = 'AA' AND mobile > '13800';
  4. 排序,但是需要用到的排序列是同一个方向进行排序的。
  5. 分组,依然需要满足 1 条件,如果用到了 group by nameInnoDB 会先将相同的 name 放在一起,然后再继续根据其他条件进行运算。

5.6 回表代价

可以这么说,如果回表的代价过高,InnoDB解析器 可能直接决定,不要使用索引,直接全表扫描。

怎么回事,就是 非聚簇 索引他的值都是连在一起的,查询的时候,称为 顺序I/O,而 非聚簇索引 查询出来的 id 并不会连在一起,所以回表去查询 id 数据的时候,称为 随机I/O。而 随机I/O 的效率是很低的,所以当命中索引的数据行总是较多的时候,不如 全表扫描 来的快,所以可能出现 SQL 已经完全命中这个索引但是解析器他就是不使用。

怎么防止这种情况发生,就是让查询索引返回的数量少,比如我们可以 LIMIT 100 去限定只查询 100 条数据,或者我们只要查询索引中出现的列,比如 id 然后再在程序中,根据 id 列表去命中缓存,名不中的再一次性批量查库就可以了。

5.7 文件排序

有时候我们去解析 EXPLAIN 我们的 SQL 语句的时候,会出现 File Sort 类型,这个类型指的是,查询出来的结果不是按照我们 SQL 指定的顺序来的,所以需要在内存中(必要时借助硬盘)来重新对数据进行排序,这个过程就是 File Sort,那如何避免这个类型出现,那就是防止上面不命中索引的情况。

5.8 索引合并

使用等值且命中两个索引的列

比方说我的用户表有 name 索引 和 mobile 索引,那这个条件 WHERE name = 'Weidan' AND mobile = '18588777777' 就可以使用两个索引,然后取 id交集,然后回表查询。但是如果 name > 'Weidan' 这种条件,就不会使用到合并。

但是但是,如果是主键列是范围的话,是可以的,因为他们合并的时候也会操作主键嘛。

不过有没有使用到,还要看成本,如果一个索引命中的数据太多,依然不会使用合并索引。

合并两个索引的列

依然需要满足 列都是等值查询 这个条件,比如 WHERE name = 'Weidan' OR mobile = '18588777777',会合并两个主键再统一回表查询。

范围搜索合并查询

不过如果我们是范围查询,又想使用两个索引怎么办,我们可以把 name > 'Weidan' 以及 monile > '18588777777' 先查询出来 id 然后 union 在一起,再通过 id 去寻找我们想要的数据。

但是还是建议把需要合并的列作为一个 非聚簇 索引来覆盖,效率要高。

七.表连接查询

首先就以最经典的学生、成绩来做示例吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
CREATE TABLE `student` (
`id` int(11) NOT NULL,
`name` varchar(50) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

BEGIN;
INSERT INTO `student` VALUES (1, '狗蛋');
INSERT INTO `student` VALUES (2, '狗剩');
INSERT INTO `student` VALUES (3, '翠花');
COMMIT;

DROP TABLE IF EXISTS `scope`;
CREATE TABLE `scope` (
`id` int(11) NOT NULL,
`subject` varchar(255) DEFAULT NULL,
`scope` int(255) DEFAULT NULL,
`stu_id` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

BEGIN;
INSERT INTO `scope` VALUES (1, '语文', 80, 1);
INSERT INTO `scope` VALUES (2, '数学', 98, 1);
INSERT INTO `scope` VALUES (3, '英语', 59, 2);
INSERT INTO `scope` VALUES (4, '政治', 79, 2);
COMMIT;

7.1 连表查询

那么先列举一个连表很普通的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
SELECT * 
FROM student, scope;

+----+--------+----+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+----+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 2 | 狗剩 | 1 | 语文 | 80 | 1 |
| 3 | 翠花 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 2 | 数学 | 98 | 1 |
| 3 | 翠花 | 2 | 数学 | 98 | 1 |
| 1 | 狗蛋 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 3 | 翠花 | 3 | 英语 | 59 | 2 |
| 1 | 狗蛋 | 4 | 政治 | 79 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
| 3 | 翠花 | 4 | 政治 | 79 | 2 |
+----+--------+----+---------+-------+--------+
12 rows in set (0.00 sec)

可以看到,如果我们不加任何条件,直接就连接两张表的话,取出来的结果就是第一张表所有的数据,去搭配第二张表所有的数据,也就是第一张表有 3 条数据,第二张表有 4 条记录,结果就有 3 ✖ 4 = 12 条记录。这个过程也称为 笛卡尔积。这种情况在日常编程中应该也没人会这么写的吧,仔细想想,如果两张表都是百万级别的数据,笛卡尔积是多恐怖的一件事情。

7.2 内连接

就是由于上面的结果集一般我们都不需要,所以我们会加上一些条件加以限制:

1
2
3
4
5
6
7
8
9
10
11
12
13
SELECT stu.*, scp.*
FROM student stu, scope scp
WHERE stu.id = scp.stu_id;

+----+--------+----+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+----+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
+----+--------+----+---------+-------+--------+
4 rows in set (0.00 sec)

这就是我们想要的结果集,所有参考学生的考试成绩。

InnoDB 是怎样的实现两个表连接的呢,这时候,优化器会根据查询成本确定 驱动表被驱动表

  • 驱动表:先查询的那个表,这里假设是 student 表,所以执行的时候,第一步执行 SELECT * FROM student;

  • 被驱动表:查询出来驱动表所有的数据以后,就会根据驱动表中每一条数据,去执行被驱动表。

    • 比如我现在 student 有三条记录,分别 id1 2 3
    • 然后对三条记录分别执行 SELECT * FROM scope WHERE stu_id = ?
    • 因为 scope 表中不存在 翠花 的成绩,所以 SELECT * FROM scope WHERE stu_id = 3 并没有展示出来,因为这个语句相当于 SELECT * FROM scope WHERE stu_id NOT NULL AND stu_id = 3

那如果上面的语句加上其他条件会怎么样,比如:

1
2
3
4
5
# 查询成绩 90 分以上的学生成绩信息
SELECT stu.*, scp.*
FROM student stu, scope scp
WHERE stu.id = scp.stu_id
AND scp.scope > 90;

那在访问驱动表 student 的时候同上面一样,但是针对驱动表中每一条数据执行被驱动表的时候,就会变成:

SELECT * FROM scope WHERE stu_id NOT NULL AND stu_id = ? AND scope > 90;

一句话就是说,这些条件针对哪个表,在执行那个表的时候就会带上指定的条件。

内连接推荐写法:

1
2
3
SELECT stu.*, scp.*
FROM student stu INNER JOIN scope scp ON stu.id = scp.stu_id
WHERE scp.scope > 90;

效率没有变高,但是可读性变好,在我看来,特定的限定功能就需要放在特定的位置,连接条件在 ON 后面,而查询条件在 WHERE 后面,也可以很好的跟下面的外连接写法统一。

7.3 外连接

外连接的目的是将连接在一起查询的表中,以某个表为主要目的,查询他在另外一个表的所有信息,包括不存在的信息。比如还是上面的例子,翠花她是个坏学生,一个科目都没有参加考试,但是老师如果通过上面的内连接查询来看的话,可能都不知道还有这个人的存在。这就麻烦大了啊,家长上门投诉,我给你钱了,她没参加考试你都不管管的吗。所以这时候,就需要外连接来做这个事情了:

1
2
3
4
5
6
7
8
9
10
11
12
SELECT * 
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id;
+----+--------+------+---------+-------+--------+
| id | name | id | subject | scope | stu_id |
+----+--------+------+---------+-------+--------+
| 1 | 狗蛋 | 1 | 语文 | 80 | 1 |
| 1 | 狗蛋 | 2 | 数学 | 98 | 1 |
| 2 | 狗剩 | 3 | 英语 | 59 | 2 |
| 2 | 狗剩 | 4 | 政治 | 79 | 2 |
| 3 | 翠花 | NULL | NULL | NULL | NULL |
+----+--------+------+---------+-------+--------+
5 rows in set (0.00 sec)

这个时候,嗯哼,一抓一个准,没有来考试的,在 scope 表的字段中,显示为 NULL

关于 驱动表被驱动表 的含义已经在上面内连接的内容中指定,那么如果说 内连接 是根据成本来指定 驱动表 的,那 外连接 就是我们来指定驱动表。

驱动表 所有满足我们条件的数据,来查询 被驱动表,如果 被驱动表 不存在 驱动表 中某条数据的关联,显示为 NULL

那么关键点在哪里,就在这个 ON 后面的条件,ON 后面的条件,是指定 被驱动表 中不满足 ON 条件情况下依然要显示的关键。(在内连接中连接条件放 ON 和放 WHERE 效果一样)

如果外连接有几层,比如说三层:

1
2
3
4
SELECT *
FROM teacher t
LEFT JOIN student ON t.id = stu.into
LEFT JOIN scope scp ON stu.id = scope.stu_id;

那么上面这条查询需要怎么走呢,先查询老师所有的学生,得到一张中间表以后,再以这张中间表作为 驱动表 来查询 scope

八.复杂条件查询

MySQL InnoDB 中存在着 SQL解析器,解析器可谓为了查询正确的数据而费尽心思,目的只有一个,就是较快的查询出来正确的数据。其实这里也可以联想到 jvm编译器,为了执行效率比较快,会对字节码进行一些顺序的重新编排,什么 happen before 规则都出来了。

而什么东西查询最快呢,那就是查询常量的时候了。

8.1 整理查询语句

首先就是去除不必要的括号,因为括号如果没有意义存在,那移除了就更有利于优化器的解析了。

比如 WHERE (id = 1) 会被优化成 WHERE id = 1。这个好像也没什么好说的。

复杂一点的 WHERE (id = 1 AND name = 'WEIDAN') AND age = 20 则会被优化成 WHERE id = 1 AND name = 'WEIDAN' AND age = 20

8.2 简化查询条件

WHERE age > 18 AND real_age > age 则直接变成 WHERE age > 18 AND real_age > 18

8.3 删除废话条件

像我之前老师教的,在写 MyBatis Mapper.xml 的时候,因为 WHERE 总是需要动态的条件,所以前面会加一个 WHERE 1 = 1(为啥不用标签,因为标签可读性其实不怎么高)。

那么,如果我写的是 WHERE 1 = 1 AND name LIKE 'Weidan%',那么语句将会被直接优化为 WHERE name LIKE 'Weidan%' 。这么想想好像直接写 WHERE 1 = 1 除了解析器多了点工作,好像也没什么所谓了。

8.4 计算表达式

WHERE age = 9 + 9 那么解析器会直接给你个 WHERE age = 18。不过如果这些计算方式放在了列名上,比如:

1
2
WHERE -age = -18
WHERE to_days(age) = 18

解析器害怕他也优化错了,所以也会放弃优化,一个简单的记忆就是函数发生在等号后面的常量值上,可以优化,但是如果需要依赖列的计算的话,那就放弃优化了。

8.5 必要时转换外连接到内连接

右外连接会被转换为左外连接,毕竟 左外连接 更好确定 驱动表被驱动表

那如果我们在查询 左外连接 的时候,暗示着我得出的结果中,被驱动表不含 NULL 的值,聪明的暖男解析器将会 GET 到这个信息,把 外连接 优化成 内连接

比如上面的例子中:

1
2
3
4
5
6
7
SELECT * 
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id
AND scp.id = 1;

SELECT *
FROM student stu LEFT JOIN scope scp ON scp.stu_id = stu.id;
AND scp.id IS NOT NULL;

那么上面两个语句中是不是就都暗示解析器中,拿到的 scope 中的 id 不能为空。

所以!直接变成这条语句执行:

1
2
3
4
5
6
SELECT * 
FROM student stu INNER JOIN scope scp ON scp.stu_id = stu.id
AND scp.id = 1;

SELECT *
FROM student stu INNER JOIN scope scp ON scp.stu_id = stu.id;;

九.完

SqlSession

好了,继上一节的 SqlSession 继续,一气呵成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
private class SqlSessionInterceptor implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
try {
// 来到这里调用SqlSession的方法
Object result = method.invoke(sqlSession, args);
if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
// force commit even on non-dirty sessions because some databases require
// a commit/rollback before calling close()
sqlSession.commit(true);
}
return result;
} catch (Throwable t) {
Throwable unwrapped = unwrapThrowable(t);
if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
// release the connection to avoid a deadlock if the translator is no loaded. See issue #22
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
sqlSession = null;
Throwable translated = SqlSessionTemplate.this.exceptionTranslator
.translateExceptionIfPossible((PersistenceException) unwrapped);
if (translated != null) {
unwrapped = translated;
}
}
throw unwrapped;
} finally {
if (sqlSession != null) {
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
}
}

OK,来到 DefaultSqlSession#selectOne

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public <T> T selectOne(String statement, Object parameter) {
// Popular vote was to return null on 0 results and throw exception on too many.
List<T> list = this.selectList(statement, parameter);
if (list.size() == 1) {
return list.get(0);
} else if (list.size() > 1) {
throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
} else {
return null;
}
}

看得到,有对象封装的情况下,无论查询一个还是多个,都是使用 List 来接收。

阅读全文 »

准备一下

由于我们使用 MyBatis 的时候,很少独立使用,所以我直接从 SpringBoot 自动装配,进入 MyBatis 来看看。 直接看 org.mybatis.spring.boot:mybatis-spring-boot-starter 源码目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
├── LICENSE
├── README.md
├── license.txt
├── mvnw
├── mvnw.cmd
├── mybatis-spring-boot-autoconfigure
│   ├── format.xml
│   ├── license.txt
│   ├── pom.xml
│   └── src
├── mybatis-spring-boot-samples
│   ├── mybatis-spring-boot-sample-annotation
│   ├── mybatis-spring-boot-sample-freemarker
│   ├── mybatis-spring-boot-sample-freemarker-legacy
│   ├── mybatis-spring-boot-sample-groovy
│   ├── mybatis-spring-boot-sample-kotlin
│   ├── mybatis-spring-boot-sample-thymeleaf
│   ├── mybatis-spring-boot-sample-velocity
│   ├── mybatis-spring-boot-sample-velocity-legacy
│   ├── mybatis-spring-boot-sample-war
│   ├── mybatis-spring-boot-sample-web
│   ├── mybatis-spring-boot-sample-xml
│   └── pom.xml
├── mybatis-spring-boot-starter
│   ├── license.txt
│   └── pom.xml
├── mybatis-spring-boot-starter-test
│   └── pom.xml
├── mybatis-spring-boot-test-autoconfigure
│   ├── format.xml
│   ├── pom.xml
│   └── src
├── pom.xml
└── travis
├── after_success.sh
└── settings.xml

mybatis-spring-boot-samples/mybatis-spring-boot-sample-xml 中刚好有我想要的示例,刚好不用自己写了,就使用这个示例来查看吧。

自动发现

从之前的自动发现文章中,我们发现,通常自动装配的类都写在 META-INF/spring.factories 这里面,现在我们可以直接查看 mybatis-spring-boot-autoconfigure 这里面的这个文件:

1
2
3
4
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration,\
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration

通过名字,感觉 org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration 在装载的过程中比较重要。 这个类里面有个内部类,是 MyBatis 切入 SpringBoot 的重点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware, ImportBeanDefinitionRegistrar {

private BeanFactory beanFactory;

@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

if (!AutoConfigurationPackages.has(this.beanFactory)) {
logger.debug("Could not determine auto-configuration package, automatic mapper scanning disabled.");
return;
}

logger.debug("Searching for mappers annotated with @Mapper");

List<String> packages = AutoConfigurationPackages.get(this.beanFactory);
if (logger.isDebugEnabled()) {
packages.forEach(pkg -> logger.debug("Using auto-configuration base package '{}'", pkg));
}

BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
builder.addPropertyValue("processPropertyPlaceHolders", true);
builder.addPropertyValue("annotationClass", Mapper.class);
builder.addPropertyValue("basePackage", StringUtils.collectionToCommaDelimitedString(packages));
BeanWrapper beanWrapper = new BeanWrapperImpl(MapperScannerConfigurer.class);
Stream.of(beanWrapper.getPropertyDescriptors())
// Need to mybatis-spring 2.0.2+
.filter(x -> x.getName().equals("lazyInitialization")).findAny()
.ifPresent(x -> builder.addPropertyValue("lazyInitialization", "${mybatis.lazy-initialization:false}"));
registry.registerBeanDefinition(MapperScannerConfigurer.class.getName(), builder.getBeanDefinition());
}

@Override
public void setBeanFactory(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
}

}

@org.springframework.context.annotation.Configuration
@Import(AutoConfiguredMapperScannerRegistrar.class)
@ConditionalOnMissingBean({ MapperFactoryBean.class, MapperScannerConfigurer.class })
public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {

@Override
public void afterPropertiesSet() {
logger.debug(
"Not found configuration for registering mapper bean using @MapperScan, MapperFactoryBean and MapperScannerConfigurer.");
}

}

这个类实现了两个接口:

  1. BeanFactoryAware:这个接口,被 Spring 扫描到的话,是会被 Spring 调用的,传递了当前 BeanFactory 实例,有助于我们后期可以对 BeanFactory 的操作;
  2. ImportBeanDefinitionRegistrar:那这个接口还真的是第一次出现,他是一个在解析 Configuration,可以注册业务需要的 BeanDefinition,简单的说,在刷新容器的时候,BeanDefinitionRegistryPostProcessors 会被调用到,而 SpringBoot 中,总是有个 ConfigurationClassPostProcessor 存在,他在解析配置的时候,会手动的去触发 ImportBeanDefinitionRegistrar#registerBeanDefinitions 函数,这时候,上面的内部类就会被读取到。

那这个类他就是负责在解析配置类的时候,注册了一个 MapperScannerConfigurer 类,看名字不用猜就知道是扫描项目中被 @Mapper 注解装饰的接口类了。

配置 SqlSession

上面注册的 MapperScannerConfigurer 是一个 BeanDefinitionRegistryPostProcessor 处理器,他会在解析配置后面,被 BeanFactory 调用到。 调用就是扫描 Mapper 的存在了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class MapperScannerConfigurer
implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware {
// ...
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
if (this.processPropertyPlaceHolders) {
processPropertyPlaceHolders();
}

// 创建扫描器
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
scanner.setAddToConfig(this.addToConfig);
scanner.setAnnotationClass(this.annotationClass);
scanner.setMarkerInterface(this.markerInterface);
// 目前SqlSessionFactory和SqlSessionTemplate均为Null
scanner.setSqlSessionFactory(this.sqlSessionFactory);
scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
scanner.setResourceLoader(this.applicationContext);
scanner.setBeanNameGenerator(this.nameGenerator);
scanner.setMapperFactoryBeanClass(this.mapperFactoryBeanClass);
if (StringUtils.hasText(lazyInitialization)) {
scanner.setLazyInitialization(Boolean.valueOf(lazyInitialization));
}
// 注册扫描过滤器,目前是加上扫描 @Mapper 的接口类
scanner.registerFilters();
// 开始扫描
scanner.scan(
StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}
// ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public int scan(String... basePackages) {
int beanCountAtScanStart = this.registry.getBeanDefinitionCount();

doScan(basePackages);

// Register annotation config processors, if necessary.
if (this.includeAnnotationConfig) {
// 一个空处理,目的是为了注册注解配置类吧?
AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
}

return (this.registry.getBeanDefinitionCount() - beanCountAtScanStart);
}

由于 ClassPathMapperScanner 继承了 Spring 提供的 ClassPathBeanDefinitionScanner,所以可以直接把扫描注解这件事情交给 Spring 来做,

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);

if (beanDefinitions.isEmpty()) {
LOGGER.warn(() -> "No MyBatis mapper was found in '" + Arrays.toString(basePackages)
+ "' package. Please check your configuration.");
} else {
processBeanDefinitions(beanDefinitions);
}

return beanDefinitions;
}

那由于上面已经扫描了大半的 BeanDefinition,后面就新增了还需要扫描 @Mapper 的注解,这时候重新进行一遍扫描以后,新增加的 BeanDefinition 就是我们要的 Mapper 接口了。所以这时候拿到了所有的 MapperBeanDefinition 进行处理。 接下来对扫描到 Mapper 进行处理一波:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
GenericBeanDefinition definition;
for (BeanDefinitionHolder holder : beanDefinitions) {
definition = (GenericBeanDefinition) holder.getBeanDefinition();
String beanClassName = definition.getBeanClassName();
LOGGER.debug(() -> "Creating MapperFactoryBean with name '" + holder.getBeanName() + "' and '" + beanClassName
+ "' mapperInterface");

// Bean是接口,但是其实现类是 MapperFactoryBean,所以这里设置 mapperFactoryBean 进去所有的 Mapper里面
definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName);
definition.setBeanClass(this.mapperFactoryBeanClass);

definition.getPropertyValues().add("addToConfig", this.addToConfig);

boolean explicitFactoryUsed = false;
// 这下面目前均为空,所以都没有进入if里面
if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) {
definition.getPropertyValues().add("sqlSessionFactory",
new RuntimeBeanReference(this.sqlSessionFactoryBeanName));
explicitFactoryUsed = true;
} else if (this.sqlSessionFactory != null) {
definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);
explicitFactoryUsed = true;
}

if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) {
if (explicitFactoryUsed) {
LOGGER.warn(
() -> "Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
}
definition.getPropertyValues().add("sqlSessionTemplate",
new RuntimeBeanReference(this.sqlSessionTemplateBeanName));
explicitFactoryUsed = true;
} else if (this.sqlSessionTemplate != null) {
if (explicitFactoryUsed) {
LOGGER.warn(
() -> "Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
}
definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate);
explicitFactoryUsed = true;
}

// 根据类型注入
if (!explicitFactoryUsed) {
LOGGER.debug(() -> "Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'.");
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
}
definition.setLazyInit(lazyInitialization);
}
}

获取Mapper

实例化在 ApplicationContext#finishBeanFactoryInitialization 中这一步实现 finishBeanFactoryInitialization。这时候需要先看下 MyBatis 自动配置配置了哪些 Bean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
public class MybatisAutoConfiguration implements InitializingBean {

@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(dataSource);
factory.setVfs(SpringBootVFS.class);
if (StringUtils.hasText(this.properties.getConfigLocation())) {
factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
}
applyConfiguration(factory);
if (this.properties.getConfigurationProperties() != null) {
factory.setConfigurationProperties(this.properties.getConfigurationProperties());
}
if (!ObjectUtils.isEmpty(this.interceptors)) {
factory.setPlugins(this.interceptors);
}
if (this.databaseIdProvider != null) {
factory.setDatabaseIdProvider(this.databaseIdProvider);
}
if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {
factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());
}
if (this.properties.getTypeAliasesSuperType() != null) {
factory.setTypeAliasesSuperType(this.properties.getTypeAliasesSuperType());
}
if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {
factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
}
if (!ObjectUtils.isEmpty(this.typeHandlers)) {
factory.setTypeHandlers(this.typeHandlers);
}
if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
factory.setMapperLocations(this.properties.resolveMapperLocations());
}
Set<String> factoryPropertyNames = Stream
.of(new BeanWrapperImpl(SqlSessionFactoryBean.class).getPropertyDescriptors()).map(PropertyDescriptor::getName)
.collect(Collectors.toSet());
Class<? extends LanguageDriver> defaultLanguageDriver = this.properties.getDefaultScriptingLanguageDriver();
if (factoryPropertyNames.contains("scriptingLanguageDrivers") && !ObjectUtils.isEmpty(this.languageDrivers)) {
// Need to mybatis-spring 2.0.2+
factory.setScriptingLanguageDrivers(this.languageDrivers);
if (defaultLanguageDriver == null && this.languageDrivers.length == 1) {
defaultLanguageDriver = this.languageDrivers[0].getClass();
}
}
if (factoryPropertyNames.contains("defaultScriptingLanguageDriver")) {
// Need to mybatis-spring 2.0.2+
factory.setDefaultScriptingLanguageDriver(defaultLanguageDriver);
}

return factory.getObject();
}

@Bean
@ConditionalOnMissingBean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
ExecutorType executorType = this.properties.getExecutorType();
if (executorType != null) {
return new SqlSessionTemplate(sqlSessionFactory, executorType);
} else {
return new SqlSessionTemplate(sqlSessionFactory);
}
}

}

SqlSessionFactorySqlSessionTemplate,先来看看这两个类的作用: 这两个类主要围绕构建 SqlSession 来做一些配置的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public interface SqlSession extends Closeable {

<T> T selectOne(String statement);

<T> T selectOne(String statement, Object parameter);

<E> List<E> selectList(String statement);

<E> List<E> selectList(String statement, Object parameter);

<E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds);

<K, V> Map<K, V> selectMap(String statement, String mapKey);

<K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey);

<K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey, RowBounds rowBounds);

<T> Cursor<T> selectCursor(String statement);

<T> Cursor<T> selectCursor(String statement, Object parameter);

<T> Cursor<T> selectCursor(String statement, Object parameter, RowBounds rowBounds);

void select(String statement, Object parameter, ResultHandler handler);

void select(String statement, ResultHandler handler);

void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler);

int insert(String statement);

int insert(String statement, Object parameter);

int update(String statement);

int update(String statement, Object parameter);

int delete(String statement);

int delete(String statement, Object parameter);

void commit();

void commit(boolean force);

void rollback();

void rollback(boolean force);

List<BatchResult> flushStatements();

@Override
void close();

void clearCache();

Configuration getConfiguration();

<T> T getMapper(Class<T> type);

Connection getConnection();
}

这是 MyBatis 执行 SQL 的关键接口,定义了 jdbc 常用的方法。而 SqlSessionTemplate 就是一个 SqlSession,专门用来整合 Spring 的一个 SqlSession,而 SqlSessionFactory 则是封装了连接的方法以及一些其他的配置,比如拦截器等等。 那我们看看主启动类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@SpringBootApplication
public class SampleXmlApplication implements CommandLineRunner {

public static void main(String[] args) {
SpringApplication.run(SampleXmlApplication.class, args);
}

private final CityDao cityDao;

private final HotelMapper hotelMapper;

public SampleXmlApplication(CityDao cityDao, HotelMapper hotelMapper) {
this.cityDao = cityDao;
this.hotelMapper = hotelMapper;
}

@Override
@SuppressWarnings("squid:S106")
public void run(String... args) {
System.out.println(this.cityDao.selectCityById(1));
System.out.println(this.hotelMapper.selectByCityId(1));
}

}

他注入了一个 CityDao 以及 一个 HotelMapper,两种不同的查询方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Component
public class CityDao {

private final SqlSession sqlSession;

public CityDao(SqlSession sqlSession) {
this.sqlSession = sqlSession;
}

public City selectCityById(long id) {
return this.sqlSession.selectOne("selectCityById", id);
}

}

@Mapper
public interface HotelMapper {

Hotel selectByCityId(int cityId);

}

那么,Spring 容器在初始化 SampleXmlApplication 的时候就会去循环递归的去找到最后所需要的依赖,那其实 CityDao 的初始化并不难,现在看看 HotelMapper 的初始化。 HotelMapper 在注册给 Spring 的时候其实已经将实际对象的 FactoryBean 设置成 MapperFactoryBean,而 MapperFactoryBean 实现了 FactoryBeanSpring 将会通过调用 getObject 来获取工厂构造的对象,所以这时候我们看看 MapperFactoryBean 怎么实现这个方法的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {

// 我们自己写的Mapper接口类
private Class<T> mapperInterface;

private boolean addToConfig = true;

public MapperFactoryBean() {
// intentionally empty
}

public MapperFactoryBean(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}

// FactoryBean需要实现这个方法
@Override
public T getObject() throws Exception {
return getSqlSession().getMapper(this.mapperInterface);
}

}

OK,我们可以看到他从 SqlSession 中来获取 Mapper代理,所以这个方法在这里充其量就是一个桥梁,架通了 MyBatis 构造的代理以及 SpringBean 的桥。 那 SqlSession 怎么来的,又是一个 Factory 构造来的,还记得上面的工厂配置类吗,就是那里构造出来的,现在我们需要回去去看看他怎么构造 MyBatis 上下文的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
// 构造一个SqlSessionFactory
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
// 数据源
factory.setDataSource(dataSource);
// 提供一个访问SpringBoot资源的实现类,是个VFS,MyBatis定义访问资源的一个抽象类
factory.setVfs(SpringBootVFS.class);
// MyBatis配置文件:mybatis.config-location=classpath:mybatis-config.xml
// 在applicationContext.yml中的配置
if (StringUtils.hasText(this.properties.getConfigLocation())) {
factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
}
// 读取MyBatisProperties配置,此时SpringBoot已经将配置文件中的信息转换为JavaBean
// 不过我们没有配置什么东西
applyConfiguration(factory);
if (this.properties.getConfigurationProperties() != null) {
factory.setConfigurationProperties(this.properties.getConfigurationProperties());
}
// MyBatis拦截器
if (!ObjectUtils.isEmpty(this.interceptors)) {
factory.setPlugins(this.interceptors);
}
// 主键Id生成器
if (this.databaseIdProvider != null) {
factory.setDatabaseIdProvider(this.databaseIdProvider);
}
// 接收数据JavaBean所在的包
if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {
factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());
}
// 接收数据JavaBean父类
if (this.properties.getTypeAliasesSuperType() != null) {
factory.setTypeAliasesSuperType(this.properties.getTypeAliasesSuperType());
}
// 类型处理器所在的包
if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {
factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
}
// 手动指定的类型处理器
if (!ObjectUtils.isEmpty(this.typeHandlers)) {
factory.setTypeHandlers(this.typeHandlers);
}
// Mapper.xml所在的位置
if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
factory.setMapperLocations(this.properties.resolveMapperLocations());
}
// 判断使用的自动SQL脚本驱动,看到这里我才知道SQL模板不仅支持xml,还支持FreeMarker/thymeleaf等模板引擎
Set<String> factoryPropertyNames = Stream
.of(new BeanWrapperImpl(SqlSessionFactoryBean.class).getPropertyDescriptors()).map(PropertyDescriptor::getName)
.collect(Collectors.toSet());
Class<? extends LanguageDriver> defaultLanguageDriver = this.properties.getDefaultScriptingLanguageDriver();
if (factoryPropertyNames.contains("scriptingLanguageDrivers") && !ObjectUtils.isEmpty(this.languageDrivers)) {
// Need to mybatis-spring 2.0.2+
factory.setScriptingLanguageDrivers(this.languageDrivers);
if (defaultLanguageDriver == null && this.languageDrivers.length == 1) {
defaultLanguageDriver = this.languageDrivers[0].getClass();
}
}
if (factoryPropertyNames.contains("defaultScriptingLanguageDriver")) {
// Need to mybatis-spring 2.0.2+
factory.setDefaultScriptingLanguageDriver(defaultLanguageDriver);
}

// 构建SqlSessionFactory,在这里做了MyBatis上下文的初始化。
return factory.getObject();
}

来到这里,终于进来了 MyBatis 的内容了,就是开始读取配置,解析,然后这个 Configuration 类将贯穿相应使用 MyBatis 的整个生命周期。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// SqlSessionFactoryBean.Java
@Override
public SqlSessionFactory getObject() throws Exception {
if (this.sqlSessionFactory == null) {
afterPropertiesSet();
}

return this.sqlSessionFactory;
}
@Override
public void afterPropertiesSet() throws Exception {
// 校验连接池是否存在
notNull(dataSource, "Property 'dataSource' is required");
notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
// 'configuration' and 'configLocation'不能同时声明
state((configuration == null && configLocation == null) !(configuration != null && configLocation != null),
"Property 'configuration' and 'configLocation' can not specified with together");

// 开始构建
this.sqlSessionFactory = buildSqlSessionFactory();
}

目前官方示例给的一个配置就是简单的扫描接收类型的包以及 Mapper 映射的路径:

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<typeAliases>
<package name="sample.mybatis.domain"/>
</typeAliases>
<mappers>
<mapper resource="sample/mybatis/mapper/CityMapper.xml"/>
<mapper resource="sample/mybatis/mapper/HotelMapper.xml"/>
</mappers>
</configuration>

进入 buildSqlSessionFactory 我们就可以看到配置熟悉的东西都在这里被解析,这个代码没点 j8 基础都看不懂了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
// SqlSessionFactoryBean.Java
protected SqlSessionFactory buildSqlSessionFactory() throws Exception {

final Configuration targetConfiguration;

// Builder模式
XMLConfigBuilder xmlConfigBuilder = null;
// 如果我们配置了 configuration 对象,这里可以减少xml的读取,有效提速吧
if (this.configuration != null) {
targetConfiguration = this.configuration;
if (targetConfiguration.getVariables() == null) {
targetConfiguration.setVariables(this.configurationProperties);
} else if (this.configurationProperties != null) {
targetConfiguration.getVariables().putAll(this.configurationProperties);
}
} else if (this.configLocation != null) {
// 构建XMLConfigBuilder,与下面的SpringBoot配置一起解析
xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
// 还没解析,只是先把默认的 Configuration 引用拿到
targetConfiguration = xmlConfigBuilder.getConfiguration();
} else {
// 没有配置,使用默认的配置
LOGGER.debug(
() -> "Property 'configuration' or 'configLocation' not specified, using default MyBatis Configuration");
targetConfiguration = new Configuration();
Optional.ofNullable(this.configurationProperties).ifPresent(targetConfiguration::setVariables);
}

// 对象工厂
Optional.ofNullable(this.objectFactory).ifPresent(targetConfiguration::setObjectFactory);
// 对象包装工厂
Optional.ofNullable(this.objectWrapperFactory).ifPresent(targetConfiguration::setObjectWrapperFactory);
// SpringBoot扫描的资源了
Optional.ofNullable(this.vfs).ifPresent(targetConfiguration::setVfsImpl);

// ------------- 下面是通过SpringBoot自动配置的方式去整合xml的配置了 -------------
if (hasLength(this.typeAliasesPackage)) {
scanClasses(this.typeAliasesPackage, this.typeAliasesSuperType).stream()
.filter(clazz -> !clazz.isAnonymousClass()).filter(clazz -> !clazz.isInterface())
.filter(clazz -> !clazz.isMemberClass()).forEach(targetConfiguration.getTypeAliasRegistry()::registerAlias);
}

if (!isEmpty(this.typeAliases)) {
Stream.of(this.typeAliases).forEach(typeAlias -> {
targetConfiguration.getTypeAliasRegistry().registerAlias(typeAlias);
LOGGER.debug(() -> "Registered type alias: '" + typeAlias + "'");
});
}

if (!isEmpty(this.plugins)) {
Stream.of(this.plugins).forEach(plugin -> {
targetConfiguration.addInterceptor(plugin);
LOGGER.debug(() -> "Registered plugin: '" + plugin + "'");
});
}

if (hasLength(this.typeHandlersPackage)) {
scanClasses(this.typeHandlersPackage, TypeHandler.class).stream().filter(clazz -> !clazz.isAnonymousClass())
.filter(clazz -> !clazz.isInterface()).filter(clazz -> !Modifier.isAbstract(clazz.getModifiers()))
.forEach(targetConfiguration.getTypeHandlerRegistry()::register);
}

if (!isEmpty(this.typeHandlers)) {
Stream.of(this.typeHandlers).forEach(typeHandler -> {
targetConfiguration.getTypeHandlerRegistry().register(typeHandler);
LOGGER.debug(() -> "Registered type handler: '" + typeHandler + "'");
});
}

if (!isEmpty(this.scriptingLanguageDrivers)) {
Stream.of(this.scriptingLanguageDrivers).forEach(languageDriver -> {
targetConfiguration.getLanguageRegistry().register(languageDriver);
LOGGER.debug(() -> "Registered scripting language driver: '" + languageDriver + "'");
});
}
Optional.ofNullable(this.defaultScriptingLanguageDriver)
.ifPresent(targetConfiguration::setDefaultScriptingLanguage);

if (this.databaseIdProvider != null) {// fix #64 set databaseId before parse mapper xmls
try {
targetConfiguration.setDatabaseId(this.databaseIdProvider.getDatabaseId(this.dataSource));
} catch (SQLException e) {
throw new NestedIOException("Failed getting a databaseId", e);
}
}

Optional.ofNullable(this.cache).ifPresent(targetConfiguration::addCache);

if (xmlConfigBuilder != null) {
try {
// 解析xml,将配置应用到Configuration中
xmlConfigBuilder.parse();
LOGGER.debug(() -> "Parsed configuration file: '" + this.configLocation + "'");
} catch (Exception ex) {
throw new NestedIOException("Failed to parse config resource: " + this.configLocation, ex);
} finally {
ErrorContext.instance().reset();
}
}

targetConfiguration.setEnvironment(new Environment(this.environment,
this.transactionFactory == null ? new SpringManagedTransactionFactory() : this.transactionFactory,
this.dataSource));

if (this.mapperLocations != null) {
if (this.mapperLocations.length == 0) {
LOGGER.warn(() -> "Property 'mapperLocations' was specified but matching resources are not found.");
} else {
for (Resource mapperLocation : this.mapperLocations) {
if (mapperLocation == null) {
continue;
}
try {
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments());
xmlMapperBuilder.parse();
} catch (Exception e) {
throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
} finally {
ErrorContext.instance().reset();
}
LOGGER.debug(() -> "Parsed mapper file: '" + mapperLocation + "'");
}
}
} else {
LOGGER.debug(() -> "Property 'mapperLocations' was not specified.");
}

// 方法就是简单的将配置放入 DefaultSqlSessionFactory 中返回出去
// 用于在项目中需要开启SqlSession的时候可以用到
return this.sqlSessionFactoryBuilder.build(targetConfiguration);

/*
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
*/


}

解析Configuration.xml

1
2
3
4
5
6
7
8
9
// XMLConfigBuilder.java
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// XMLConfigBuilder.java
private void parseConfiguration(XNode root) {
try {
// 解析属性元素
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}

这里大概就是 Configuration.xml 的所有内容的解析,那我们就直接看看解析 SqlMapper 的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// <mapper resource="sample/mybatis/mapper/HotelMapper.xml"/>
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
// 获取 resource
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
if (resource != null && url == null && mapperClass == null) {
// 来到这里开始解析
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
// 解析的重点
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}

解析Mapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// XMLMapperBuilder.java
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
// 解析解析所有标签
configurationElement(parser.evalNode("/mapper"));
// 标记已经解析完成
configuration.addLoadedResource(resource);
// 注册接口和xml绑定的类型
bindMapperForNamespace();
}

// 解析ResultMaps
parsePendingResultMaps();
// 解析缓存
parsePendingCacheRefs();
// 解析SQL语句
parsePendingStatements();
}

解析Statement语句

主要是解析标签比较重要,我们现在进入来看看 configurationElement(parser.evalNode("/mapper")) 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
buildStatementFromContext(context.evalNodes("selectinsertupdatedelete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}

结合 Mapper.xml 来看看:

1
2
3
4
5
6
7
8
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="sample.mybatis.mapper.HotelMapper">
<select id="selectByCityId" resultType="Hotel">
select city, name, address, zip from hotel where city = #{id}
</select>
</mapper>

解析语句是发生在 buildStatementFromContext 里面的:

1
2
3
4
5
6
7
private void buildStatementFromContext(List<XNode> list) {
// 根据不同的数据库id来绑定语句
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
buildStatementFromContext(list, null);
}

不得不说,MyBatis 还真的是喜欢 Builder

1
2
3
4
5
6
7
8
9
10
11
12
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
// 循环每一个 statement 标签,开始解析
for (XNode context : list) {
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
// 发生解析,将 mapper.xml 的信息存入 Configuration
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
public void parseStatementNode() {
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");

if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}

String nodeName = context.getNode().getNodeName();
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

// Include Fragments before parsing
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());

String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);

String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);

// Parse selectKey after includes and remove them.
processSelectKeyNodes(id, parameterTypeClass, langDriver);

// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
KeyGenerator keyGenerator;
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}

// 构建SQLSource对象,这个接口将动态SQL和静态的划分成两个不同的对象
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
// 获取判断是否是预编译的SQL语句
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
// 获取返回的对象类型
String resultType = context.getStringAttribute("resultType");
// 结合配置文件中的typeAliases来获取到Class对象
Class<?> resultTypeClass = resolveClass(resultType);
String resultMap = context.getStringAttribute("resultMap");
String resultSetType = context.getStringAttribute("resultSetType");
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
if (resultSetTypeEnum == null) {
resultSetTypeEnum = configuration.getDefaultResultSetType();
}
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
String resultSets = context.getStringAttribute("resultSets");

// 通过builderAssistant将Mapper对象塞入Configuration
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}

通过构建好的配置信息,将查询的配置加入 Configuration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// builderAssistant.addMappedStatement
public MappedStatement addMappedStatement(
String id,
SqlSource sqlSource,
StatementType statementType,
SqlCommandType sqlCommandType,
Integer fetchSize,
Integer timeout,
String parameterMap,
Class<?> parameterType,
String resultMap,
Class<?> resultType,
ResultSetType resultSetType,
boolean flushCache,
boolean useCache,
boolean resultOrdered,
KeyGenerator keyGenerator,
String keyProperty,
String keyColumn,
String databaseId,
LanguageDriver lang,
String resultSets) {

if (unresolvedCacheRef) {
throw new IncompleteElementException("Cache-ref not yet resolved");
}

// 获取id,sample.mybatis.mapper.HotelMapper.selectByCityId
id = applyCurrentNamespace(id, false);
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;

MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
.resource(resource)
.fetchSize(fetchSize)
.timeout(timeout)
.statementType(statementType)
.keyGenerator(keyGenerator)
.keyProperty(keyProperty)
.keyColumn(keyColumn)
.databaseId(databaseId)
.lang(lang)
.resultOrdered(resultOrdered)
.resultSets(resultSets)
.resultMaps(getStatementResultMaps(resultMap, resultType, id))
.resultSetType(resultSetType)
.flushCacheRequired(valueOrDefault(flushCache, !isSelect))
.useCache(valueOrDefault(useCache, isSelect))
.cache(currentCache);

ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
if (statementParameterMap != null) {
statementBuilder.parameterMap(statementParameterMap);
}

MappedStatement statement = statementBuilder.build();
// 存入configuration
configuration.addMappedStatement(statement);
return statement;
}

注册Mapper到MyBatis的Registry

Configuration 中,解析 Mapper 已经完成,接下来就是注册一个接口代理对象到 Configuration 中去:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
private void bindMapperForNamespace() {
String namespace = builderAssistant.getCurrentNamespace();
if (namespace != null) {
Class<?> boundType = null;
try {
// 解析接口类型
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException e) {
//ignore, bound type is not required
}
if (boundType != null) {
if (!configuration.hasMapper(boundType)) {
// Spring may not know the real resource name so we set a flag
// to prevent loading again this resource from the mapper interface
// look at MapperAnnotationBuilder#loadXmlResource
configuration.addLoadedResource("namespace:" + namespace);
// 将当前接口类型添加到Configuration
configuration.addMapper(boundType);
}
}
}
}

重点就在于这个 `configuration.addMapper` 中:

public class Configuration {
// ...
public <T> void addMapper(Class<T> type) {
mapperRegistry.addMapper(type);
}
// ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MapperRegistry {
// ...
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
// 使用 MapperProxyFactory 封装接口类型,加入Registery中
knownMappers.put(type, new MapperProxyFactory<>(type));
// 加入后,继续解析这个接口,看看有没有 @Select 之类的注解存在
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
// ...
}

MapperFactoryBean对接Spring

还记得上面对接 SpringgetBean 的对接桥梁吗,就是 MapperFactoryBean 来适配接口的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {
// ...
@Override
public T getObject() throws Exception {
return getSqlSession().getMapper(this.mapperInterface);
}
// ...
}

public class SqlSessionTemplate implements SqlSession, DisposableBean {
// ...
@Override
public <T> T getMapper(Class<T> type) {
return getConfiguration().getMapper(type, this);
}
// ...
}

public class Configuration {
// ...
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
// ...
}

public class MapperRegistry {
// ...
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
// ...
}

public class MapperProxyFactory<T> {
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}

public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
}

到达这里,初始化工作基本已经完成,现在我们要看看运行时怎么获取数据的


获取代理对象

那么现在就回到主程序中,来观察 MyBatis 是怎么封装 Mapper 接口来查询数据的,这里我重新贴一下主程序中的查询入口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@SpringBootApplication
public class SampleXmlApplication implements CommandLineRunner {

public static void main(String[] args) {
SpringApplication.run(SampleXmlApplication.class, args);
}

private final CityDao cityDao;

private final HotelMapper hotelMapper;

public SampleXmlApplication(CityDao cityDao, HotelMapper hotelMapper) {
this.cityDao = cityDao;
this.hotelMapper = hotelMapper;
}

@Override
@SuppressWarnings("squid:S106")
public void run(String... args) {
System.out.println(this.cityDao.selectCityById(1));
// 常用的是这种方式,我直接看这里了
System.out.println(this.hotelMapper.selectByCityId(1));
}

}

刚刚上面提到过,MyBatis 在整合 Spring 的时候其实就将自己 Registry 中的 MapperProxy 给放入 BeanDefinition 中,然后交给 Spring 去自动注入到需要依赖的对象。那么现在拿到的对象就是一个 MapperProxy。 既然是 MapperProxy,那要看怎么封装查询数据就是看这个了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (method.isDefault()) {
if (privateLookupInMethod == null) {
return invokeDefaultMethodJava8(proxy, method, args);
} else {
return invokeDefaultMethodJava9(proxy, method, args);
}
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}

MapperProxy 是何方神圣,哦,是一个 InvocationHandler,这是一个我从来没见过的 jdk 接口。J8 专有的动态代理的接口,那他有什么用呢,就是将实现封装在这个实现类里边,实现类有个 invoke 方法需要实现,那么实现类的所有方法被调用时,都需要先走这个方法,这时候我们就可以在 invoke 里面做一些羞羞的事情。

InvocationHandler代理

那就来示例一下这个东西怎么用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 一个普通的接口
public interface A {
void add();
}

// 代理处理器
public class InvokeHandler<T> implements InvocationHandler {

Class<T> type;

public InvokeHandler(Class<T> tClass) {
this.type = tClass;
}

// 将会拦截接口的方法
@Override
public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
System.out.println("调用到这里来了");
return null;
}

// 创建一个代理对象,接口中方法被调用的时候,均为走上面的invoke方法
public T getProxy() {
return (T) Proxy.newProxyInstance(type.getClassLoader(),
new Class[]{type}, this);
}

}


public class TestInvoke {

public static void main(String[] args) {
A proxy = new InvokeHandler<>(A.class).getProxy();
proxy.add();// 输出:调用到这里来了
}

}

那就是说,MapperProxy 拦截了我们所有接口的调用,然后,从这里读取了 mapper.xml 中配置的 SQL 语句,然后调用 jdbc 做相对应的查询。

获取MapperMethod

OK,弄清楚了动态代理,现在回来看看怎么被调用的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (method.isDefault()) {
if (privateLookupInMethod == null) {
return invokeDefaultMethodJava8(proxy, method, args);
} else {
return invokeDefaultMethodJava9(proxy, method, args);
}
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
// 上面是j8后新增的默认方法的读取,以及判断代理的方法是否是对象
// 如果哪个为是,直接调用实现的方法

// 这里开始就是MyBatis自己的实现
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}


private MapperMethod cachedMapperMethod(Method method) {
// 从缓存命中,如果没有就创建
return methodCache.computeIfAbsent(method,
k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}

MapperMethod调用

先来看看 MapperMethod 有什么属性,方法肯定就是上面的 execute 是重中之重:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
public class MapperMethod {

// 这是两个内部类对象,一个封装SQL一个封装方法的签名
private final SqlCommand command;
private final MethodSignature method;

public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
this.command = new SqlCommand(config, mapperInterface, method);
this.method = new MethodSignature(config, mapperInterface, method);
}

public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
// 根据不同的SQL类型去调用不同的方法
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
// 查询进入这里
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
// 由于主程序只是查询一个,所以跑这里来了

// 如果带有@Param则要根据名字匹配,如果没有,则根据args的顺序来查找
Object param = method.convertArgsToSqlCommandParam(args);
// sqlSession=SqlSessionTemplate,一个对接Spring事务管理器的对象
result = sqlSession.selectOne(command.getName(), param);
// 如果是j8的Optional返回的话,需要使用Optional封装结果
if (method.returnsOptional()
&& (result == null !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}
// ......
}

SqlSession拦截器

上面的代码我们来到了 sqlSession.selectOne 这句话,所以进入代码看看是发生什么事情:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
public class SqlSessionTemplate implements SqlSession, DisposableBean {

//...
@Override
public <T> T selectOne(String statement, Object parameter) {
return this.sqlSessionProxy.selectOne(statement, parameter);
}
//...

/**
* Proxy needed to route MyBatis method calls to the proper SqlSession got from Spring's Transaction Manager It also
* unwraps exceptions thrown by {@code Method#invoke(Object, Object...)} to pass a {@code PersistenceException} to the
* {@code PersistenceExceptionTranslator}.
* 翻译:代理需要路由MyBatis去调用SpringTX产生出来的SqlSession
*/
private class SqlSessionInterceptor implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 调用下面的方法去获取兼容SpringTX的SqlSession
SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
try {
// 执行方法内容 真正的SqlSession的SelectOne函数
Object result = method.invoke(sqlSession, args);
if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
// force commit even on non-dirty sessions because some databases require
// a commit/rollback before calling close()
// 提交事务
sqlSession.commit(true);
}
return result;
} catch (Throwable t) {
Throwable unwrapped = unwrapThrowable(t);
if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
// 也是关闭回滚
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
sqlSession = null;
Throwable translated = SqlSessionTemplate.this.exceptionTranslator
.translateExceptionIfPossible((PersistenceException) unwrapped);
if (translated != null) {
unwrapped = translated;
}
}
throw unwrapped;
} finally {
// 关闭SqlSession并且将连接放回连接池
if (sqlSession != null) {
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
}
}

public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator) {

notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);

// 获取SpringTX事务的适配器
SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);

SqlSession session = sessionHolder(executorType, holder);
if (session != null) {
return session;
}

LOGGER.debug(() -> "Creating a new SqlSession");
session = sessionFactory.openSession(executorType);

// 如果有Spring的事务注解的话,则会将事务注册到SpringTX模块中
registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);

return session;
}

}

查询方法

好,总体的开启 SqlSession 啊,对接事务的,有个了解,现在具体到怎么查询的问题,那就是 SqlSession 了。 算了,下集一起说……. To Be Continue...

准备个需求

之前读其他无关数据库源码的时候,只是简单的依赖了 spring-boot-starter-web,现在由于需要数据库的参与,所以需要加上 MySQL 的驱动,以及一个最简单的 jdbc 框架。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>

application.yml 配置数据库连接信息。

1
2
3
4
5
spring:
datasource:
url: jdbc:mysql://192.168.1.152:3306/spring_trans?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true
username: root
password: root

一个接收需要插入数据库的请求:

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
public class UserController {

@Autowired
private UserService userService;

@PostMapping("users")
public void add(@RequestParam("name") String name, @RequestParam("orgName") String orgName) {
userService.add(name, orgName);
}

}

由于示例,就使用最简单的例子,插入一个用户以及他所属的组织机构,一对一的关系。使用 JdbcTemplate 直接插入 SQL 语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public interface UserService {
void add(String name, String orgName);
}
@Service
public class UserServiceImpl implements UserService {

@Autowired
private JdbcTemplate jdbcTemplate;

@Override
@Transactional
public void add(String name, String orgName) {
String userId = UUID.randomUUID().toString();
jdbcTemplate.update("insert into user_info(user_uuid, user_name)\n" +
"values (?,?);", userId, name);
// 使用一个RuntimeEx来触发事务回滚
int i = 1 / 0;
String orgUUID = UUID.randomUUID().toString();
jdbcTemplate.update("insert into org_info(org_uuid, org_name, user_uuid)\n" +
"VALUES (?, ?, ?);", orgUUID, orgName, userId);
}

}

应用事务

  • 编程式注解:使用 @Transactional 修饰的类或者方法,具有事务特性。(SpringBoot 时代一般用这个)
  • 声明式事务:通过定义配置文件,需要定义一套基于公司内部的方法命名规范。如以下的配置,update 以及 insert 为名的方法,使用事务,不然使用只读事务。
1
2
3
4
5
6
7
8
9
10
11
<tx:advice id="advice" transaction-manager="transactionManager">  
<tx:attributes>
<tx:method name="update*" propagation="REQUIRED" read-only="false" rollback-for="java.lang.Exception"/>
<tx:method name="insert" propagation="REQUIRED" read-only="false"/>
</tx:attributes>
</tx:advice>

<aop:config>
<aop:pointcut id="testService" expression="execution (* com.liweidan.service.MyBatisService.*(..))"/>
<aop:advisor advice-ref="advice" pointcut-ref="testService"/>
</aop:config>

在上面例子中的 UserServiceImpl 是使用编程式事务处理的,当没有加上 @Transactional 时,在 int i = 1 / 0; 处发生异常时,上面已经插入的用户信息并不会被回滚,这就不符合我们日常的业务需求了。OK,从这里开始将要开始看看 Spring 是如何应用事务到我们的代码上的。

依赖结构

准备个需求

之前读其他无关数据库源码的时候,只是简单的依赖了 spring-boot-starter-web,现在由于需要数据库的参与,所以需要加上 MySQL 的驱动,以及一个最简单的 jdbc 框架。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>

application.yml 配置数据库连接信息。

1
2
3
4
5
spring:
datasource:
url: jdbc:mysql://192.168.1.152:3306/spring_trans?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true
username: root
password: root

一个接收需要插入数据库的请求:

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
public class UserController {

@Autowired
private UserService userService;

@PostMapping("users")
public void add(@RequestParam("name") String name, @RequestParam("orgName") String orgName) {
userService.add(name, orgName);
}

}

由于示例,就使用最简单的例子,插入一个用户以及他所属的组织机构,一对一的关系。使用 JdbcTemplate 直接插入 SQL 语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public interface UserService {
void add(String name, String orgName);
}
@Service
public class UserServiceImpl implements UserService {

@Autowired
private JdbcTemplate jdbcTemplate;

@Override
@Transactional
public void add(String name, String orgName) {
String userId = UUID.randomUUID().toString();
jdbcTemplate.update("insert into user_info(user_uuid, user_name)\n" +
"values (?,?);", userId, name);
// 使用一个RuntimeEx来触发事务回滚
int i = 1 / 0;
String orgUUID = UUID.randomUUID().toString();
jdbcTemplate.update("insert into org_info(org_uuid, org_name, user_uuid)\n" +
"VALUES (?, ?, ?);", orgUUID, orgName, userId);
}

}

应用事务

  • 编程式注解:使用 @Transactional 修饰的类或者方法,具有事务特性。(SpringBoot 时代一般用这个)
  • 声明式事务:通过定义配置文件,需要定义一套基于公司内部的方法命名规范。如以下的配置,update 以及 insert 为名的方法,使用事务,不然使用只读事务。
1
2
3
4
5
6
7
8
9
10
11
<tx:advice id="advice" transaction-manager="transactionManager">  
<tx:attributes>
<tx:method name="update*" propagation="REQUIRED" read-only="false" rollback-for="java.lang.Exception"/>
<tx:method name="insert" propagation="REQUIRED" read-only="false"/>
</tx:attributes>
</tx:advice>

<aop:config>
<aop:pointcut id="testService" expression="execution (* com.liweidan.service.MyBatisService.*(..))"/>
<aop:advisor advice-ref="advice" pointcut-ref="testService"/>
</aop:config>

在上面例子中的 UserServiceImpl 是使用编程式事务处理的,当没有加上 @Transactional 时,在 int i = 1 / 0; 处发生异常时,上面已经插入的用户信息并不会被回滚,这就不符合我们日常的业务需求了。OK,从这里开始将要开始看看 Spring 是如何应用事务到我们的代码上的。

依赖结构

OK,简单看看我们依赖了 spring-boot-starter-jdbc 都依赖了些什么东西。从图中可以看到,spring-jdbcspring-tx 以及 HicariCP 都进来了,spring-jdbc 模块也还好,封装了 JdbcTemplate 等一些偏向于原生 JDBC 的操作,spring-tx 就是此次需要说的重点了,事务管理模块,主要管理 Spring 自己定义的一些事务模型。HicariCP 则是一个高性能的数据库连接池。

基于AOP

我记得很清楚的,《从零开始架构WEB系统》中说到,可以使用 AOP 方式切入项目,获取 Connection 保存在线程的 ThreadLocal 中,在调用插入更新的时候,取出来开启事务,方法执行结束后判定是否是正常执行,如果遇到异常了,就 roolBack 插入的数据,否则 commit。那我们现在大概可以带着这个思路来看 Spring 源码。

自动配置

上一篇我们说过,SpringBoot 会自动扫描导入 org.springframework.boot.autoconfigure 包下的所有的配置,这次,事务的自动配置是 org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration。因为导入项目已经有 PlatformTransactionManager 类,所以该配置将被自动执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// TransactionAutoConfiguration.java: 
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(PlatformTransactionManager.class)
@AutoConfigureAfter({ JtaAutoConfiguration.class, HibernateJpaAutoConfiguration.class,
DataSourceTransactionManagerAutoConfiguration.class, Neo4jDataAutoConfiguration.class })
@EnableConfigurationProperties(TransactionProperties.class)
public class TransactionAutoConfiguration {

@Bean
@ConditionalOnMissingBean
public TransactionManagerCustomizers platformTransactionManagerCustomizers(
ObjectProvider<PlatformTransactionManagerCustomizer<?>> customizers) {
return new TransactionManagerCustomizers(customizers.orderedStream().collect(Collectors.toList()));
}

@Bean
@ConditionalOnMissingBean
@ConditionalOnSingleCandidate(ReactiveTransactionManager.class)
public TransactionalOperator transactionalOperator(ReactiveTransactionManager transactionManager) {
return TransactionalOperator.create(transactionManager);
}

@Configuration(proxyBeanMethods = false)
@ConditionalOnSingleCandidate(PlatformTransactionManager.class)
public static class TransactionTemplateConfiguration {

@Bean
@ConditionalOnMissingBean(TransactionOperations.class)
public TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) {
return new TransactionTemplate(transactionManager);
}

}

@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(TransactionManager.class)
@ConditionalOnMissingBean(AbstractTransactionManagementConfiguration.class)
public static class EnableTransactionManagementConfiguration {

@Configuration(proxyBeanMethods = false)
// 重点是这个注解,导入了些东西
@EnableTransactionManagement(proxyTargetClass = false)
@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false",
matchIfMissing = false)
public static class JdkDynamicAutoProxyConfiguration {

}

@Configuration(proxyBeanMethods = false)
@EnableTransactionManagement(proxyTargetClass = true)
@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
matchIfMissing = true)
public static class CglibAutoProxyConfiguration {

}

}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// EnableTransactionManagement.java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
// 导入一个Selector
@Import(TransactionManagementConfigurationSelector.class)
public @interface EnableTransactionManagement {

/**
* Indicate whether subclass-based (CGLIB) proxies are to be created ({@code true}) as
* opposed to standard Java interface-based proxies ({@code false}). The default is
* {@code false}. <strong>Applicable only if {@link #mode()} is set to
* {@link AdviceMode#PROXY}</strong>.
* <p>Note that setting this attribute to {@code true} will affect <em>all</em>
* Spring-managed beans requiring proxying, not just those marked with
* {@code @Transactional}. For example, other beans marked with Spring's
* {@code @Async} annotation will be upgraded to subclass proxying at the same
* time. This approach has no negative impact in practice unless one is explicitly
* expecting one type of proxy vs another, e.g. in tests.
*/
boolean proxyTargetClass() default false;

/**
* Indicate how transactional advice should be applied.
* <p><b>The default is {@link AdviceMode#PROXY}.</b>
* Please note that proxy mode allows for interception of calls through the proxy
* only. Local calls within the same class cannot get intercepted that way; an
* {@link Transactional} annotation on such a method within a local call will be
* ignored since Spring's interceptor does not even kick in for such a runtime
* scenario. For a more advanced mode of interception, consider switching this to
* {@link AdviceMode#ASPECTJ}.
*/
AdviceMode mode() default AdviceMode.PROXY;

/**
* Indicate the ordering of the execution of the transaction advisor
* when multiple advices are applied at a specific joinpoint.
* <p>The default is {@link Ordered#LOWEST_PRECEDENCE}.
*/
int order() default Ordered.LOWEST_PRECEDENCE;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// TransactionManagementConfigurationSelector.java
public class TransactionManagementConfigurationSelector extends AdviceModeImportSelector<EnableTransactionManagement> {

/**
* Returns {@link ProxyTransactionManagementConfiguration} or
* {@code AspectJ(Jta)TransactionManagementConfiguration} for {@code PROXY}
* and {@code ASPECTJ} values of {@link EnableTransactionManagement#mode()},
* respectively.
*/
@Override
protected String[] selectImports(AdviceMode adviceMode) {
switch (adviceMode) {
case PROXY:
return new String[] {AutoProxyRegistrar.class.getName(),
ProxyTransactionManagementConfiguration.class.getName()};
case ASPECTJ:
return new String[] {determineTransactionAspectClass()};
default:
return null;
}
}

private String determineTransactionAspectClass() {
return (ClassUtils.isPresent("javax.transaction.Transactional", getClass().getClassLoader()) ?
TransactionManagementConfigUtils.JTA_TRANSACTION_ASPECT_CONFIGURATION_CLASS_NAME :
TransactionManagementConfigUtils.TRANSACTION_ASPECT_CONFIGURATION_CLASS_NAME);
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
Collection<SourceClass> importCandidates, boolean checkForCircularImports) {

if (importCandidates.isEmpty()) {
return;
}

if (checkForCircularImports && isChainedImportOnStack(configClass)) {
this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
}
else {
this.importStack.push(configClass);
try {
for (SourceClass candidate : importCandidates) {
if (candidate.isAssignable(ImportSelector.class)) {
// Candidate class is an ImportSelector -> delegate to it to determine imports
Class<?> candidateClass = candidate.loadClass();
ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class,
this.environment, this.resourceLoader, this.registry);
if (selector instanceof DeferredImportSelector) {
this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
}
else {
// 在导入其他配置类的时候会使用上面的 selectImports 函数导入配置类
// 然后重新解析配置
String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames);
processImports(configClass, currentSourceClass, importSourceClasses, false);
}
// .....
}
}
}
}

来到这里,配置类的东西就已经被注册到 BeanFactory 中去,那么据之前的 SpringAOP 的介绍,BeanFactory 会在每次初始化 Bean 的时候,调用 BeanPostProcessor#postProcessAfterInitialization 这个函数来创建真实 Bean 的代理对象。 然后这件事情就交给了 AOP模块InfrastructureAdvisorAutoProxyCreator 来实现包装代理模式。

篇幅关系我只放关键的代码片段,快速过一下前面的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
throws BeanCreationException {
// ......
Object exposedObject = bean;
try {
populateBean(beanName, mbd, instanceWrapper);
// 包装实际Bean
exposedObject = initializeBean(beanName, exposedObject, mbd);
}
catch (Throwable ex) {
if (ex instanceof BeanCreationException && beanName.equals(((BeanCreationException) ex).getBeanName())) {
throw (BeanCreationException) ex;
}
else {
throw new BeanCreationException(
mbd.getResourceDescription(), beanName, "Initialization of bean failed", ex);
}
}
// ......
}
}
// initializeBean
protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) {
if (System.getSecurityManager() != null) {
AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
invokeAwareMethods(beanName, bean);
return null;
}, getAccessControlContext());
}
else {
invokeAwareMethods(beanName, bean);
}

Object wrappedBean = bean;
if (mbd == null !mbd.isSynthetic()) {
wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
}

try {
invokeInitMethods(beanName, wrappedBean, mbd);
}
catch (Throwable ex) {
throw new BeanCreationException(
(mbd != null ? mbd.getResourceDescription() : null),
beanName, "Invocation of init method failed", ex);
}
if (mbd == null !mbd.isSynthetic()) {
// 创建完成,使用集成的后处理器处理对应的Bean
wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
}

return wrappedBean;
}
// applyBeanPostProcessorsAfterInitialization使用后处理器包装Bean
public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName)
throws BeansException {

Object result = existingBean;
for (BeanPostProcessor processor : getBeanPostProcessors()) {
// 轮询到InfrastructureAdvisorAutoProxyCreator或AnnotationAwareAspectJAutoProxyCreator进行代理处理
Object current = processor.postProcessAfterInitialization(result, beanName);
if (current == null) {
return result;
}
result = current;
}
return result;
}

注:使用 InfrastructureAdvisorAutoProxyCreator 还是 AnnotationAwareAspectJAutoProxyCreator 取决于当前项目有没有依赖 Spring-AOP 模块,如果依赖 Spring-AOP 模块则会交给 AnnotationAwareAspectJAutoProxyCreator 进行处理。 两者的区别是:InfrastructureAdvisorAutoProxyCreator 不负责织入用户自定义的 AOP 类

然后,AbstractAdvisorAutoProxyCreator (两者的共同父类) 会找到所有的 Advisor 实现来织入这个实际对象的方法中。 因为在解析配置类的时候,BeanFactoryTransactionAttributeSourceAdvisor 这个类已经被注册到 BeanFactoryBeanDefinitionMap 中,所以第一个 Bean(大概率都是 Spring 自己需要的 Bean)创建的时候,就会触发上面自动配置中,创建 BeanFactoryTransactionAttributeSourceAdvisor 的配置类,调用方法进行相对应的创建。 然后又走了一遍 getBean 啊,doCreateBean 啊….创建 BeanFactoryTransactionAttributeSourceAdvisor。 好了,走完创建 BeanFactoryTransactionAttributeSourceAdvisor 的过程。我们现在需要看看怎么被织入。

织入方法

至于判定的方法,之前已经说过基于 @PointCut 切入,那么这个解析方式,应该很容易想到了吧,就是 类注解 + 方法级别注解 判定是否要切入当前方法。 上面两个织入类,无论怎么样都会来到这个函数织入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
return bean;
}
if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
return bean;
}
if (isInfrastructureClass(bean.getClass()) shouldSkip(bean.getClass(), beanName)) {
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}

// 获取当前Bean所需要的AOP拦截器链
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
if (specificInterceptors != DO_NOT_PROXY) {
this.advisedBeans.put(cacheKey, Boolean.TRUE);
// 进入创建代理对象
Object proxy = createProxy(
bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
this.proxyTypes.put(cacheKey, proxy.getClass());
return proxy;
}

this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}

进入配置并使用 ProxyFactory 来创建代理对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
@Nullable Object[] specificInterceptors, TargetSource targetSource) {

if (this.beanFactory instanceof ConfigurableListableBeanFactory) {
AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass);
}

ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.copyFrom(this);

if (!proxyFactory.isProxyTargetClass()) {
if (shouldProxyTargetClass(beanClass, beanName)) {
proxyFactory.setProxyTargetClass(true);
}
else {
evaluateProxyInterfaces(beanClass, proxyFactory);
}
}

Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
proxyFactory.addAdvisors(advisors);
proxyFactory.setTargetSource(targetSource);
customizeProxyFactory(proxyFactory);

proxyFactory.setFrozen(this.freezeProxy);
if (advisorsPreFiltered()) {
proxyFactory.setPreFiltered(true);
}

return proxyFactory.getProxy(getProxyClassLoader());
}

进入 getProxy 方法,通过配置来指定配置工厂(jdk代理cglib),然后调用两个工厂都有的 getProxy 来获取代理实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public Object getProxy(@Nullable ClassLoader classLoader) {
return createAopProxy().getProxy(classLoader);
}
protected final synchronized AopProxy createAopProxy() {
if (!this.active) {
activate();
}
return getAopProxyFactory().createAopProxy(this);
}
// DefaultAopProxyFactory.java
@Override
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
if (config.isOptimize() config.isProxyTargetClass() hasNoUserSuppliedProxyInterfaces(config)) {
Class<?> targetClass = config.getTargetClass();
if (targetClass == null) {
throw new AopConfigException("TargetSource cannot determine target class: " +
"Either an interface or a target is required for proxy creation.");
}
if (targetClass.isInterface() Proxy.isProxyClass(targetClass)) {
return new JdkDynamicAopProxy(config);
}
// 虽然我的类是带有接口的,但是来到这里,使用cglib进行创建
return new ObjenesisCglibAopProxy(config);
}
else {
return new JdkDynamicAopProxy(config);
}
}

SpringBoot代理模式

上面跑出来的小问题,为啥带有接口还是使用了 cglib 创建,跟我之前读 Spring-AOP 的时候是不一样的结果。 这里就要说到 SpringBoot 的自动创建配置了:

1
2
3
4
5
6
7
// META-INF/additional-spring-configuration-metadata.json
{
"name": "spring.aop.proxy-target-class",
"type": "java.lang.Boolean",
"description": "Whether subclass-based (CGLIB) proxies are to be created (true), as opposed to standard Java interface-based proxies (false).",
"defaultValue": true
},

然而搜遍谷歌 Spring 项目,只看到 Spring 成员说了一句:

意思是 Cglib 代理能够减少类转换异常。

织入拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Override
public Object getProxy(@Nullable ClassLoader classLoader) {
// 省略部分代码
// Configure CGLIB Enhancer...
Enhancer enhancer = createEnhancer();
if (classLoader != null) {
enhancer.setClassLoader(classLoader);
if (classLoader instanceof SmartClassLoader &&
((SmartClassLoader) classLoader).isClassReloadable(proxySuperClass)) {
enhancer.setUseCache(false);
}
}
enhancer.setSuperclass(proxySuperClass);
enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised));
enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
enhancer.setStrategy(new ClassLoaderAwareGeneratorStrategy(classLoader));

// 在Callback这里即会织入BeanFactoryTransactionAttributeSourceAdvisor中的拦截器:
// TransactionInterceptor
Callback[] callbacks = getCallbacks(rootClass);
Class<?>[] types = new Class<?>[callbacks.length];
for (int x = 0; x < types.length; x++) {
types[x] = callbacks[x].getClass();
}
// fixedInterceptorMap only populated at this point, after getCallbacks call above
enhancer.setCallbackFilter(new ProxyCallbackFilter(
this.advised.getConfigurationOnlyCopy(), this.fixedInterceptorMap, this.fixedInterceptorOffset));
enhancer.setCallbackTypes(types);

// Generate the proxy class and create a proxy instance.
return createProxyClassAndInstance(enhancer, callbacks);
}
catch (CodeGenerationException IllegalArgumentException ex) {
throw new AopConfigException("Could not generate CGLIB subclass of " + this.advised.getTargetClass() +
": Common causes of this problem include using a final class or a non-visible class",
ex);
}
catch (Throwable ex) {
// TargetSource.getTarget() failed
throw new AopConfigException("Unexpected AOP exception", ex);
}
}

拦截器织入完成接下来就需要看看怎么被调用的了。


插入事务

真男人要直接,直接在 Controller 打断点,进入业务方法之前,会先进入 TransactionInterceptor#invoke

1
2
3
4
5
6
7
8
9
@Override
@Nullable
public Object invoke(MethodInvocation invocation) throws Throwable {
// 获取目标类
Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);

// 开启事务执行方法
return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
}

呃,代码还是挺长的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
@Nullable
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
final InvocationCallback invocation) throws Throwable {

// 获取所有配置的事务属性元数据类,如果为null,代表默认事务(默认是autoCommit=true)
TransactionAttributeSource tas = getTransactionAttributeSource();
// 从缓存中命中当前方法的事务配置
final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
// 获取事务管理器
final TransactionManager tm = determineTransactionManager(txAttr);

// Reactive环境走这里,先跳过
if (this.reactiveAdapterRegistry != null && tm instanceof ReactiveTransactionManager) {
ReactiveTransactionSupport txSupport = this.transactionSupportCache.computeIfAbsent(method, key -> {
if (KotlinDetector.isKotlinType(method.getDeclaringClass()) && KotlinDelegate.isSuspend(method)) {
throw new TransactionUsageException(
"Unsupported annotated transaction on suspending function detected: " + method +
". Use TransactionalOperator.transactional extensions instead.");
}
ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(method.getReturnType());
if (adapter == null) {
throw new IllegalStateException("Cannot apply reactive transaction to non-reactive return type: " +
method.getReturnType());
}
return new ReactiveTransactionSupport(adapter);
});
return txSupport.invokeWithinTransaction(
method, targetClass, invocation, txAttr, (ReactiveTransactionManager) tm);
}

// 获取Spring提供的统一事务管理器
PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);

if (txAttr == null !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
// 创建或者加入当前事务
TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);

Object retVal;
try {
// 执行目标方法,如果有下一个AOP链则接下去执行
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// 遇到异常,回滚数据库事务
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
cleanupTransactionInfo(txInfo);
}

// 响应式数据库操作 也是先跳过
if (vavrPresent && VavrDelegate.isVavrTry(retVal)) {
// Set rollback-only in case of Vavr failure matching our rollback rules...
TransactionStatus status = txInfo.getTransactionStatus();
if (status != null && txAttr != null) {
retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
}
}

// 提交数据库事务
commitTransactionAfterReturning(txInfo);
return retVal;
}
// CallbackPreferringPlatformTransactionManager 可回调的管理器
// 也是响应式的内容
else {
final ThrowableHolder throwableHolder = new ThrowableHolder();

// It's a CallbackPreferringPlatformTransactionManager: pass a TransactionCallback in.
try {
Object result = ((CallbackPreferringPlatformTransactionManager) ptm).execute(txAttr, status -> {
TransactionInfo txInfo = prepareTransactionInfo(ptm, txAttr, joinpointIdentification, status);
try {
Object retVal = invocation.proceedWithInvocation();
if (vavrPresent && VavrDelegate.isVavrTry(retVal)) {
// Set rollback-only in case of Vavr failure matching our rollback rules...
retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
}
return retVal;
}
catch (Throwable ex) {
if (txAttr.rollbackOn(ex)) {
// A RuntimeException: will lead to a rollback.
if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}
else {
throw new ThrowableHolderException(ex);
}
}
else {
// A normal return value: will lead to a commit.
throwableHolder.throwable = ex;
return null;
}
}
finally {
cleanupTransactionInfo(txInfo);
}
});

// Check result state: It might indicate a Throwable to rethrow.
if (throwableHolder.throwable != null) {
throw throwableHolder.throwable;
}
return result;
}
catch (ThrowableHolderException ex) {
throw ex.getCause();
}
catch (TransactionSystemException ex2) {
if (throwableHolder.throwable != null) {
logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
ex2.initApplicationException(throwableHolder.throwable);
}
throw ex2;
}
catch (Throwable ex2) {
if (throwableHolder.throwable != null) {
logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
}
throw ex2;
}
}
}

那下面就一步一步来看。

TransactionAttribute事务属性

这是一个定义了事务级别以及其他比如超时信息的事务元信息类,当前获取的是一个 RuleBasedTransactionAttribute 基于一定规则的事务属性,定义了必须在抛出 Runtime 异常的时候回滚数据库。 父级是 TransactionDefinition,这可是一个元老级别的类了,定义了事务管理器常见所需的隔离级别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
public interface TransactionDefinition {

// ------------------------------ Spring事务隔离级别 ------------------------------
// 如果当前没有事务,则创建一个新事务
int PROPAGATION_REQUIRED = 0;

// 如果当前没有事务则以无事务的方式运行
int PROPAGATION_SUPPORTS = 1;

// 当前有事务就加入没有就抛异常
int PROPAGATION_MANDATORY = 2;

// 阻塞当前事务,创建一个子事务
int PROPAGATION_REQUIRES_NEW = 3;

// 一直以无事务的状态运行
int PROPAGATION_NOT_SUPPORTED = 4;

// 如果当前有事务则抛出异常
int PROPAGATION_NEVER = 5;

// 如果存在事务则在嵌套事务中运行
// 行为类似于 PROPAGATION_REQUIRED
int PROPAGATION_NESTED = 6;

// ------------------------------ jdbc原生事务隔离级别 ------------------------------

// 使用数据库默认的级别
int ISOLATION_DEFAULT = -1;

// 未提交读
int ISOLATION_READ_UNCOMMITTED = 1; // same as java.sql.Connection.TRANSACTION_READ_UNCOMMITTED;

// 不可重复读
int ISOLATION_READ_COMMITTED = 2; // same as java.sql.Connection.TRANSACTION_READ_COMMITTED;


// 可重复读
int ISOLATION_REPEATABLE_READ = 4; // same as java.sql.Connection.TRANSACTION_REPEATABLE_READ;

// 串行执行事务
int ISOLATION_SERIALIZABLE = 8; // same as java.sql.Connection.TRANSACTION_SERIALIZABLE;


/**
* Use the default timeout of the underlying transaction system,
* or none if timeouts are not supported.
*/
int TIMEOUT_DEFAULT = -1;


/**
* Return the propagation behavior.
* <p>Must return one of the {@code PROPAGATION_XXX} constants
* defined on {@link TransactionDefinition this interface}.
* <p>The default is {@link #PROPAGATION_REQUIRED}.
* @return the propagation behavior
* @see #PROPAGATION_REQUIRED
* @see org.springframework.transaction.support.TransactionSynchronizationManager#isActualTransactionActive()
*/
default int getPropagationBehavior() {
return PROPAGATION_REQUIRED;
}

/**
* Return the isolation level.
* <p>Must return one of the {@code ISOLATION_XXX} constants defined on
* {@link TransactionDefinition this interface}. Those constants are designed
* to match the values of the same constants on {@link java.sql.Connection}.
* <p>Exclusively designed for use with {@link #PROPAGATION_REQUIRED} or
* {@link #PROPAGATION_REQUIRES_NEW} since it only applies to newly started
* transactions. Consider switching the "validateExistingTransactions" flag to
* "true" on your transaction manager if you'd like isolation level declarations
* to get rejected when participating in an existing transaction with a different
* isolation level.
* <p>The default is {@link #ISOLATION_DEFAULT}. Note that a transaction manager
* that does not support custom isolation levels will throw an exception when
* given any other level than {@link #ISOLATION_DEFAULT}.
* @return the isolation level
* @see #ISOLATION_DEFAULT
* @see org.springframework.transaction.support.AbstractPlatformTransactionManager#setValidateExistingTransaction
*/
default int getIsolationLevel() {
return ISOLATION_DEFAULT;
}

/**
* Return the transaction timeout.
* <p>Must return a number of seconds, or {@link #TIMEOUT_DEFAULT}.
* <p>Exclusively designed for use with {@link #PROPAGATION_REQUIRED} or
* {@link #PROPAGATION_REQUIRES_NEW} since it only applies to newly started
* transactions.
* <p>Note that a transaction manager that does not support timeouts will throw
* an exception when given any other timeout than {@link #TIMEOUT_DEFAULT}.
* <p>The default is {@link #TIMEOUT_DEFAULT}.
* @return the transaction timeout
*/
default int getTimeout() {
return TIMEOUT_DEFAULT;
}

/**
* Return whether to optimize as a read-only transaction.
* <p>The read-only flag applies to any transaction context, whether backed
* by an actual resource transaction ({@link #PROPAGATION_REQUIRED}/
* {@link #PROPAGATION_REQUIRES_NEW}) or operating non-transactionally at
* the resource level ({@link #PROPAGATION_SUPPORTS}). In the latter case,
* the flag will only apply to managed resources within the application,
* such as a Hibernate {@code Session}.
* <p>This just serves as a hint for the actual transaction subsystem;
* it will <i>not necessarily</i> cause failure of write access attempts.
* A transaction manager which cannot interpret the read-only hint will
* <i>not</i> throw an exception when asked for a read-only transaction.
* @return {@code true} if the transaction is to be optimized as read-only
* ({@code false} by default)
* @see org.springframework.transaction.support.TransactionSynchronization#beforeCommit(boolean)
* @see org.springframework.transaction.support.TransactionSynchronizationManager#isCurrentTransactionReadOnly()
*/
default boolean isReadOnly() {
return false;
}

/**
* Return the name of this transaction. Can be {@code null}.
* <p>This will be used as the transaction name to be shown in a
* transaction monitor, if applicable (for example, WebLogic's).
* <p>In case of Spring's declarative transactions, the exposed name will be
* the {@code fully-qualified class name + "." + method name} (by default).
* @return the name of this transaction ({@code null} by default}
* @see org.springframework.transaction.interceptor.TransactionAspectSupport
* @see org.springframework.transaction.support.TransactionSynchronizationManager#getCurrentTransactionName()
*/
@Nullable
default String getName() {
return null;
}


// Static builder methods

/**
* Return an unmodifiable {@code TransactionDefinition} with defaults.
* <p>For customization purposes, use the modifiable
* {@link org.springframework.transaction.support.DefaultTransactionDefinition}
* instead.
* @since 5.2
*/
static TransactionDefinition withDefaults() {
return StaticTransactionDefinition.INSTANCE;
}

}

那这个东西用在哪里呢,后面就会看到处理方式了,先放一放。

事务管理器

这一步没做什么,就当是单纯获得了一个事务管理器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Nullable
protected TransactionManager determineTransactionManager(@Nullable TransactionAttribute txAttr) {
// Do not attempt to lookup tx manager if no tx attributes are set
if (txAttr == null this.beanFactory == null) {
return getTransactionManager();
}

String qualifier = txAttr.getQualifier();
if (StringUtils.hasText(qualifier)) {
return determineQualifiedTransactionManager(this.beanFactory, qualifier);
}
else if (StringUtils.hasText(this.transactionManagerBeanName)) {
return determineQualifiedTransactionManager(this.beanFactory, this.transactionManagerBeanName);
}
else {
TransactionManager defaultTransactionManager = getTransactionManager();
if (defaultTransactionManager == null) {
defaultTransactionManager = this.transactionManagerCache.get(DEFAULT_TRANSACTION_MANAGER_KEY);
if (defaultTransactionManager == null) {
defaultTransactionManager = this.beanFactory.getBean(TransactionManager.class);
this.transactionManagerCache.putIfAbsent(
DEFAULT_TRANSACTION_MANAGER_KEY, defaultTransactionManager);
}
}
return defaultTransactionManager;
}
}

开启数据库事务

可以看到,传递了一个事务定义元数据来创建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
protected TransactionInfo createTransactionIfNecessary(@Nullable PlatformTransactionManager tm,
@Nullable TransactionAttribute txAttr, final String joinpointIdentification) {

// 处理名字
if (txAttr != null && txAttr.getName() == null) {
txAttr = new DelegatingTransactionAttribute(txAttr) {
@Override
public String getName() {
return joinpointIdentification;
}
};
}

TransactionStatus status = null;
if (txAttr != null) {
if (tm != null) {
status = tm.getTransaction(txAttr);
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Skipping transactional joinpoint [" + joinpointIdentification +
"] because no transaction manager has been configured");
}
}
}
// 管理当前开启的事务管理器
return prepareTransactionInfo(tm, txAttr, joinpointIdentification, status);
}

首先,先根据当前的事务管理级别创建 TransactionStatus

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
@Override
public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
throws TransactionException {

// 获取 TransactionDefinition,如果配置中没有定义则获取默认的事务定义.
TransactionDefinition def = (definition != null ? definition : TransactionDefinition.withDefaults());

// 获取事务状态对象
Object transaction = doGetTransaction();
boolean debugEnabled = logger.isDebugEnabled();

if (isExistingTransaction(transaction)) {
// 当前存在事务,根据不同等级返回 TransactionStatus.
return handleExistingTransaction(def, transaction, debugEnabled);
}

// 超时的话抛出异常.
if (def.getTimeout() < TransactionDefinition.TIMEOUT_DEFAULT) {
throw new InvalidTimeoutException("Invalid transaction timeout", def.getTimeout());
}

// 不存在事务的时候,需要根据上面的事务定义来继续行为.
if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) {
// 这个等级是没有事务即抛出异常
throw new IllegalTransactionStateException(
"No existing transaction found for transaction marked with propagation 'mandatory'");
}
else if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED
def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW
def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
SuspendedResourcesHolder suspendedResources = suspend(null);
if (debugEnabled) {
logger.debug("Creating new transaction with name [" + def.getName() + "]: " + def);
}
try {
// 创建新的 TransactionStatus 然后开启数据库事务
boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
// 创建一个新的DefaultTransactionStatus贯穿整个事务
DefaultTransactionStatus status = newTransactionStatus(
def, transaction, true, newSynchronization, debugEnabled, suspendedResources);
// 开启新事务
doBegin(transaction, def);
// 设置激活程序中的事务管理器,设置只读属性、事务名字、隔离级别等信息
prepareSynchronization(status, def);
return status;
}
catch (RuntimeException Error ex) {
resume(null, suspendedResources);
throw ex;
}
}
else {
// Create "empty" transaction: no actual transaction, but potentially synchronization.
if (def.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT && logger.isWarnEnabled()) {
logger.warn("Custom isolation level specified but no actual transaction initiated; " +
"isolation level will effectively be ignored: " + def);
}
boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
return prepareTransactionStatus(def, null, true, newSynchronization, debugEnabled, null);
}
}

OK,接下来看看 doBegin 怎么管理 Connection

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// 开启事务管理器
@Override
protected void doBegin(Object transaction, TransactionDefinition definition) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
Connection con = null;

try {
if (!txObject.hasConnectionHolder()
txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
// 通过数据库连接池拿到 Connection
Connection newCon = obtainDataSource().getConnection();
if (logger.isDebugEnabled()) {
logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
}
// 使用ConnectionHolder管理当前的连接
txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
}

txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
con = txObject.getConnectionHolder().getConnection();

// Connection属性设置(之前的隔离级别、是否只读)
Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
txObject.setPreviousIsolationLevel(previousIsolationLevel);
txObject.setReadOnly(definition.isReadOnly());

// 修改Connection的只读状态
if (con.getAutoCommit()) {
txObject.setMustRestoreAutoCommit(true);
if (logger.isDebugEnabled()) {
logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
}
con.setAutoCommit(false);
}

// 如果当前是只读事务则发送 SET TRANSACTION READ ONLY 给数据库切换事务状态
prepareTransactionalConnection(con, definition);
txObject.getConnectionHolder().setTransactionActive(true);

int timeout = determineTimeout(definition);
if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
}

// 把ConnectionHolder绑定到TransactionSynchronizationManager
if (txObject.isNewConnectionHolder()) {
TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());
}
}

catch (Throwable ex) {
if (txObject.isNewConnectionHolder()) {
DataSourceUtils.releaseConnection(con, obtainDataSource());
txObject.setConnectionHolder(null, false);
}
throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex);
}
}

开始执行

为了防止需要拖拉到上面去阅读,我先把之前执行到哪个地方放在这里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
@Nullable
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
final InvocationCallback invocation) throws Throwable {

// 获取所有配置的事务属性元数据类,如果为null,代表默认事务(默认是autoCommit=true)
TransactionAttributeSource tas = getTransactionAttributeSource();
// 从缓存中命中当前方法的事务配置
final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
// 获取事务管理器
final TransactionManager tm = determineTransactionManager(txAttr);

// Reactive环境走这里,先跳过
if (this.reactiveAdapterRegistry != null && tm instanceof ReactiveTransactionManager) {
ReactiveTransactionSupport txSupport = this.transactionSupportCache.computeIfAbsent(method, key -> {
if (KotlinDetector.isKotlinType(method.getDeclaringClass()) && KotlinDelegate.isSuspend(method)) {
throw new TransactionUsageException(
"Unsupported annotated transaction on suspending function detected: " + method +
". Use TransactionalOperator.transactional extensions instead.");
}
ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(method.getReturnType());
if (adapter == null) {
throw new IllegalStateException("Cannot apply reactive transaction to non-reactive return type: " +
method.getReturnType());
}
return new ReactiveTransactionSupport(adapter);
});
return txSupport.invokeWithinTransaction(
method, targetClass, invocation, txAttr, (ReactiveTransactionManager) tm);
}

// 获取Spring提供的统一事务管理器
PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);

if (txAttr == null !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
// 创建或者加入当前事务
TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);

Object retVal;
try {
// 执行目标方法,如果有下一个AOP链则接下去执行
// ---------------> 已经准备好了事务信息,可以开始执行整个AOP链条了
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// 遇到异常,回滚数据库事务
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
// 清空当前事务,如果当前是嵌套事务,管理器会重新拿到之前的事务
cleanupTransactionInfo(txInfo);
}

// 响应式数据库操作 也是先跳过
if (vavrPresent && VavrDelegate.isVavrTry(retVal)) {
// Set rollback-only in case of Vavr failure matching our rollback rules...
TransactionStatus status = txInfo.getTransactionStatus();
if (status != null && txAttr != null) {
retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
}
}

// 提交数据库事务
commitTransactionAfterReturning(txInfo);
return retVal;
}
// CallbackPreferringPlatformTransactionManager 可回调的管理器
// 也是响应式的内容
else {
// 省略响应式内容.........
}
}

正常提交

拿到事务管理器,提交事务:

1
2
3
4
5
6
7
8
protected void commitTransactionAfterReturning(@Nullable TransactionInfo txInfo) {
if (txInfo != null && txInfo.getTransactionStatus() != null) {
if (logger.isTraceEnabled()) {
logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() + "]");
}
txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
}
}

需要经过判断来做是否真正提交:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Override
public final void commit(TransactionStatus status) throws TransactionException {
if (status.isCompleted()) {
throw new IllegalTransactionStateException(
"Transaction is already completed - do not call commit or rollback more than once per transaction");
}

DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;

// 根据用户TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();设置
// 是否不让提交(比如校验没有通过,但是没有抛出异常) 如果设置了则回滚数据库
if (defStatus.isLocalRollbackOnly()) {
if (defStatus.isDebug()) {
logger.debug("Transactional code has requested rollback");
}
processRollback(defStatus, false);
return;
}

// 这里是判断全局事务中配置了只能在全局回滚并且当前已经出现错误,如果当前是内部事务,会标记业务层事务出现错误
if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) {
if (defStatus.isDebug()) {
logger.debug("Global transaction is marked as rollback-only but transactional code requested commit");
}
processRollback(defStatus, true);
return;
}

// 处理提交事务
processCommit(defStatus);
}

处理提交,可以说,我们项目中使用到提交事务生命周期都在这里被调用到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
private void processCommit(DefaultTransactionStatus status) throws TransactionException {
try {
boolean beforeCompletionInvoked = false;

try {
boolean unexpectedRollback = false;
// 下面三个是调用相对应的生命周期
prepareForCommit(status);
triggerBeforeCommit(status);
triggerBeforeCompletion(status);
beforeCompletionInvoked = true;

// SavePoint 先不看
if (status.hasSavepoint()) {
if (status.isDebug()) {
logger.debug("Releasing transaction savepoint");
}
unexpectedRollback = status.isGlobalRollbackOnly();
status.releaseHeldSavepoint();
}
// 当前是一个新的事务
else if (status.isNewTransaction()) {
if (status.isDebug()) {
logger.debug("Initiating transaction commit");
}
unexpectedRollback = status.isGlobalRollbackOnly();
// 真正做提交的地方
doCommit(status);
}
else if (isFailEarlyOnGlobalRollbackOnly()) {
unexpectedRollback = status.isGlobalRollbackOnly();
}

// Throw UnexpectedRollbackException if we have a global rollback-only
// marker but still didn't get a corresponding exception from commit.
if (unexpectedRollback) {
throw new UnexpectedRollbackException(
"Transaction silently rolled back because it has been marked as rollback-only");
}
}
catch (UnexpectedRollbackException ex) {
// can only be caused by doCommit
triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK);
throw ex;
}
catch (TransactionException ex) {
// can only be caused by doCommit
if (isRollbackOnCommitFailure()) {
doRollbackOnCommitException(status, ex);
}
else {
triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN);
}
throw ex;
}
catch (RuntimeException Error ex) {
if (!beforeCompletionInvoked) {
triggerBeforeCompletion(status);
}
doRollbackOnCommitException(status, ex);
throw ex;
}

// Trigger afterCommit callbacks, with an exception thrown there
// propagated to callers but the transaction still considered as committed.
try {
// 调用事务生命周期的后处理器
triggerAfterCommit(status);
}
finally {
triggerAfterCompletion(status, TransactionSynchronization.STATUS_COMMITTED);
}

}
finally {
cleanupAfterCompletion(status);
}
}

拿到 Connection 调用 Commit 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
protected void doCommit(DefaultTransactionStatus status) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
Connection con = txObject.getConnectionHolder().getConnection();
if (status.isDebug()) {
logger.debug("Committing JDBC transaction on Connection [" + con + "]");
}
try {
con.commit();
}
catch (SQLException ex) {
throw new TransactionSystemException("Could not commit JDBC transaction", ex);
}
}

回滚数据库

业务代码加一个除以 0 的计算:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Service
public class UserServiceImpl implements UserService {

@Autowired
private JdbcTemplate jdbcTemplate;

@Override
@Transactional
public void add(String name, String orgName) {
String userId = UUID.randomUUID().toString();
jdbcTemplate.update("insert into user_info(user_uuid, user_name)\n" +
"values (?,?);", userId, name);
int i = 1 / 0;
String orgUUID = UUID.randomUUID().toString();
jdbcTemplate.update("insert into org_info(org_uuid, org_name, user_uuid)\n" +
"VALUES (?, ?, ?);", orgUUID, orgName, userId);
}

}

那么出现了异常了,这一步会来到 try-catch 中的 catch:

1
2
3
4
5
6
7
8
9
10
11
12
try {
// 执行目标方法,如果有下一个AOP链则接下去执行
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// 遇到异常,回滚数据库事务
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
cleanupTransactionInfo(txInfo);
}

这个是简单了很多了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
if (txInfo != null && txInfo.getTransactionStatus() != null) {
if (logger.isTraceEnabled()) {
logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() +
"] after exception: " + ex);
}
// 判断当前抛出的异常是否需要回滚数据库
if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
try {
txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
}
catch (TransactionSystemException ex2) {
logger.error("Application exception overridden by rollback exception", ex);
ex2.initApplicationException(ex);
throw ex2;
}
catch (RuntimeException Error ex2) {
logger.error("Application exception overridden by rollback exception", ex);
throw ex2;
}
}
// 如果不会滚,继续提交
else {
// We don't roll back on this exception.
// Will still roll back if TransactionStatus.isRollbackOnly() is true.
try {
txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
}
catch (TransactionSystemException ex2) {
logger.error("Application exception overridden by commit exception", ex);
ex2.initApplicationException(ex);
throw ex2;
}
catch (RuntimeException Error ex2) {
logger.error("Application exception overridden by commit exception", ex);
throw ex2;
}
}
}
}

好吧来到了处理方法:

1
2
3
4
5
6
7
8
9
10
@Override
public final void rollback(TransactionStatus status) throws TransactionException {
if (status.isCompleted()) {
throw new IllegalTransactionStateException(
"Transaction is already completed - do not call commit or rollback more than once per transaction");
}

DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;
processRollback(defStatus, false);
}

那么其实跟提交的套路是一样的,触发一系列的生命周期:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
private void processRollback(DefaultTransactionStatus status, boolean unexpected) {
try {
boolean unexpectedRollback = unexpected;

try {
triggerBeforeCompletion(status);

if (status.hasSavepoint()) {
if (status.isDebug()) {
logger.debug("Rolling back transaction to savepoint");
}
status.rollbackToHeldSavepoint();
}
else if (status.isNewTransaction()) {
if (status.isDebug()) {
logger.debug("Initiating transaction rollback");
}
doRollback(status);
}
else {
// Participating in larger transaction
if (status.hasTransaction()) {
if (status.isLocalRollbackOnly() isGlobalRollbackOnParticipationFailure()) {
if (status.isDebug()) {
logger.debug("Participating transaction failed - marking existing transaction as rollback-only");
}
doSetRollbackOnly(status);
}
else {
if (status.isDebug()) {
logger.debug("Participating transaction failed - letting transaction originator decide on rollback");
}
}
}
else {
logger.debug("Should roll back transaction but cannot - no transaction available");
}
// Unexpected rollback only matters here if we're asked to fail early
if (!isFailEarlyOnGlobalRollbackOnly()) {
unexpectedRollback = false;
}
}
}
catch (RuntimeException Error ex) {
triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN);
throw ex;
}

triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK);

// Raise UnexpectedRollbackException if we had a global rollback-only marker
if (unexpectedRollback) {
throw new UnexpectedRollbackException(
"Transaction rolled back because it has been marked as rollback-only");
}
}
finally {
cleanupAfterCompletion(status);
}
}

拿到 Connection 进行回滚。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
protected void doRollback(DefaultTransactionStatus status) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
Connection con = txObject.getConnectionHolder().getConnection();
if (status.isDebug()) {
logger.debug("Rolling back JDBC transaction on Connection [" + con + "]");
}
try {
con.rollback();
}
catch (SQLException ex) {
throw new TransactionSystemException("Could not roll back JDBC transaction", ex);
}
}

OK,简单看看我们依赖了 spring-boot-starter-jdbc 都依赖了些什么东西。从图中可以看到,spring-jdbcspring-tx 以及 HicariCP 都进来了,spring-jdbc 模块也还好,封装了 JdbcTemplate 等一些偏向于原生 JDBC 的操作,spring-tx 就是此次需要说的重点了,事务管理模块,主要管理 Spring 自己定义的一些事务模型。HicariCP 则是一个高性能的数据库连接池。

基于AOP

我记得很清楚的,《从零开始架构WEB系统》中说到,可以使用 AOP 方式切入项目,获取 Connection 保存在线程的 ThreadLocal 中,在调用插入更新的时候,取出来开启事务,方法执行结束后判定是否是正常执行,如果遇到异常了,就 roolBack 插入的数据,否则 commit。那我们现在大概可以带着这个思路来看 Spring 源码。

自动配置

上一篇我们说过,SpringBoot 会自动扫描导入 org.springframework.boot.autoconfigure 包下的所有的配置,这次,事务的自动配置是 org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration。因为导入项目已经有 PlatformTransactionManager 类,所以该配置将被自动执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// TransactionAutoConfiguration.java: 
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(PlatformTransactionManager.class)
@AutoConfigureAfter({ JtaAutoConfiguration.class, HibernateJpaAutoConfiguration.class,
DataSourceTransactionManagerAutoConfiguration.class, Neo4jDataAutoConfiguration.class })
@EnableConfigurationProperties(TransactionProperties.class)
public class TransactionAutoConfiguration {

@Bean
@ConditionalOnMissingBean
public TransactionManagerCustomizers platformTransactionManagerCustomizers(
ObjectProvider<PlatformTransactionManagerCustomizer<?>> customizers) {
return new TransactionManagerCustomizers(customizers.orderedStream().collect(Collectors.toList()));
}

@Bean
@ConditionalOnMissingBean
@ConditionalOnSingleCandidate(ReactiveTransactionManager.class)
public TransactionalOperator transactionalOperator(ReactiveTransactionManager transactionManager) {
return TransactionalOperator.create(transactionManager);
}

@Configuration(proxyBeanMethods = false)
@ConditionalOnSingleCandidate(PlatformTransactionManager.class)
public static class TransactionTemplateConfiguration {

@Bean
@ConditionalOnMissingBean(TransactionOperations.class)
public TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) {
return new TransactionTemplate(transactionManager);
}

}

@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(TransactionManager.class)
@ConditionalOnMissingBean(AbstractTransactionManagementConfiguration.class)
public static class EnableTransactionManagementConfiguration {

@Configuration(proxyBeanMethods = false)
// 重点是这个注解,导入了些东西
@EnableTransactionManagement(proxyTargetClass = false)
@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false",
matchIfMissing = false)
public static class JdkDynamicAutoProxyConfiguration {

}

@Configuration(proxyBeanMethods = false)
@EnableTransactionManagement(proxyTargetClass = true)
@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
matchIfMissing = true)
public static class CglibAutoProxyConfiguration {

}

}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// EnableTransactionManagement.java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
// 导入一个Selector
@Import(TransactionManagementConfigurationSelector.class)
public @interface EnableTransactionManagement {

/**
* Indicate whether subclass-based (CGLIB) proxies are to be created ({@code true}) as
* opposed to standard Java interface-based proxies ({@code false}). The default is
* {@code false}. <strong>Applicable only if {@link #mode()} is set to
* {@link AdviceMode#PROXY}</strong>.
* <p>Note that setting this attribute to {@code true} will affect <em>all</em>
* Spring-managed beans requiring proxying, not just those marked with
* {@code @Transactional}. For example, other beans marked with Spring's
* {@code @Async} annotation will be upgraded to subclass proxying at the same
* time. This approach has no negative impact in practice unless one is explicitly
* expecting one type of proxy vs another, e.g. in tests.
*/
boolean proxyTargetClass() default false;

/**
* Indicate how transactional advice should be applied.
* <p><b>The default is {@link AdviceMode#PROXY}.</b>
* Please note that proxy mode allows for interception of calls through the proxy
* only. Local calls within the same class cannot get intercepted that way; an
* {@link Transactional} annotation on such a method within a local call will be
* ignored since Spring's interceptor does not even kick in for such a runtime
* scenario. For a more advanced mode of interception, consider switching this to
* {@link AdviceMode#ASPECTJ}.
*/
AdviceMode mode() default AdviceMode.PROXY;

/**
* Indicate the ordering of the execution of the transaction advisor
* when multiple advices are applied at a specific joinpoint.
* <p>The default is {@link Ordered#LOWEST_PRECEDENCE}.
*/
int order() default Ordered.LOWEST_PRECEDENCE;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// TransactionManagementConfigurationSelector.java
public class TransactionManagementConfigurationSelector extends AdviceModeImportSelector<EnableTransactionManagement> {

/**
* Returns {@link ProxyTransactionManagementConfiguration} or
* {@code AspectJ(Jta)TransactionManagementConfiguration} for {@code PROXY}
* and {@code ASPECTJ} values of {@link EnableTransactionManagement#mode()},
* respectively.
*/
@Override
protected String[] selectImports(AdviceMode adviceMode) {
switch (adviceMode) {
case PROXY:
return new String[] {AutoProxyRegistrar.class.getName(),
ProxyTransactionManagementConfiguration.class.getName()};
case ASPECTJ:
return new String[] {determineTransactionAspectClass()};
default:
return null;
}
}

private String determineTransactionAspectClass() {
return (ClassUtils.isPresent("javax.transaction.Transactional", getClass().getClassLoader()) ?
TransactionManagementConfigUtils.JTA_TRANSACTION_ASPECT_CONFIGURATION_CLASS_NAME :
TransactionManagementConfigUtils.TRANSACTION_ASPECT_CONFIGURATION_CLASS_NAME);
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
Collection<SourceClass> importCandidates, boolean checkForCircularImports) {

if (importCandidates.isEmpty()) {
return;
}

if (checkForCircularImports && isChainedImportOnStack(configClass)) {
this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
}
else {
this.importStack.push(configClass);
try {
for (SourceClass candidate : importCandidates) {
if (candidate.isAssignable(ImportSelector.class)) {
// Candidate class is an ImportSelector -> delegate to it to determine imports
Class<?> candidateClass = candidate.loadClass();
ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class,
this.environment, this.resourceLoader, this.registry);
if (selector instanceof DeferredImportSelector) {
this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
}
else {
// 在导入其他配置类的时候会使用上面的 selectImports 函数导入配置类
// 然后重新解析配置
String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames);
processImports(configClass, currentSourceClass, importSourceClasses, false);
}
// .....
}
}
}
}

来到这里,配置类的东西就已经被注册到 BeanFactory 中去,那么据之前的 SpringAOP 的介绍,BeanFactory 会在每次初始化 Bean 的时候,调用 BeanPostProcessor#postProcessAfterInitialization 这个函数来创建真实 Bean 的代理对象。 然后这件事情就交给了 AOP模块InfrastructureAdvisorAutoProxyCreator 来实现包装代理模式。

篇幅关系我只放关键的代码片段,快速过一下前面的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
throws BeanCreationException {
// ......
Object exposedObject = bean;
try {
populateBean(beanName, mbd, instanceWrapper);
// 包装实际Bean
exposedObject = initializeBean(beanName, exposedObject, mbd);
}
catch (Throwable ex) {
if (ex instanceof BeanCreationException && beanName.equals(((BeanCreationException) ex).getBeanName())) {
throw (BeanCreationException) ex;
}
else {
throw new BeanCreationException(
mbd.getResourceDescription(), beanName, "Initialization of bean failed", ex);
}
}
// ......
}
}
// initializeBean
protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) {
if (System.getSecurityManager() != null) {
AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
invokeAwareMethods(beanName, bean);
return null;
}, getAccessControlContext());
}
else {
invokeAwareMethods(beanName, bean);
}

Object wrappedBean = bean;
if (mbd == null !mbd.isSynthetic()) {
wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
}

try {
invokeInitMethods(beanName, wrappedBean, mbd);
}
catch (Throwable ex) {
throw new BeanCreationException(
(mbd != null ? mbd.getResourceDescription() : null),
beanName, "Invocation of init method failed", ex);
}
if (mbd == null !mbd.isSynthetic()) {
// 创建完成,使用集成的后处理器处理对应的Bean
wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
}

return wrappedBean;
}
// applyBeanPostProcessorsAfterInitialization使用后处理器包装Bean
public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName)
throws BeansException {

Object result = existingBean;
for (BeanPostProcessor processor : getBeanPostProcessors()) {
// 轮询到InfrastructureAdvisorAutoProxyCreator或AnnotationAwareAspectJAutoProxyCreator进行代理处理
Object current = processor.postProcessAfterInitialization(result, beanName);
if (current == null) {
return result;
}
result = current;
}
return result;
}

注:使用 InfrastructureAdvisorAutoProxyCreator 还是 AnnotationAwareAspectJAutoProxyCreator 取决于当前项目有没有依赖 Spring-AOP 模块,如果依赖 Spring-AOP 模块则会交给 AnnotationAwareAspectJAutoProxyCreator 进行处理。 两者的区别是:InfrastructureAdvisorAutoProxyCreator 不负责织入用户自定义的 AOP 类

然后,AbstractAdvisorAutoProxyCreator (两者的共同父类) 会找到所有的 Advisor 实现来织入这个实际对象的方法中。 因为在解析配置类的时候,BeanFactoryTransactionAttributeSourceAdvisor 这个类已经被注册到 BeanFactoryBeanDefinitionMap 中,所以第一个 Bean(大概率都是 Spring 自己需要的 Bean)创建的时候,就会触发上面自动配置中,创建 BeanFactoryTransactionAttributeSourceAdvisor 的配置类,调用方法进行相对应的创建。 然后又走了一遍 getBean 啊,doCreateBean 啊….创建 BeanFactoryTransactionAttributeSourceAdvisor。 好了,走完创建 BeanFactoryTransactionAttributeSourceAdvisor 的过程。我们现在需要看看怎么被织入。

织入方法

至于判定的方法,之前已经说过基于 @PointCut 切入,那么这个解析方式,应该很容易想到了吧,就是 类注解 + 方法级别注解 判定是否要切入当前方法。 上面两个织入类,无论怎么样都会来到这个函数织入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
return bean;
}
if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
return bean;
}
if (isInfrastructureClass(bean.getClass()) shouldSkip(bean.getClass(), beanName)) {
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}

// 获取当前Bean所需要的AOP拦截器链
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
if (specificInterceptors != DO_NOT_PROXY) {
this.advisedBeans.put(cacheKey, Boolean.TRUE);
// 进入创建代理对象
Object proxy = createProxy(
bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
this.proxyTypes.put(cacheKey, proxy.getClass());
return proxy;
}

this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}

进入配置并使用 ProxyFactory 来创建代理对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
@Nullable Object[] specificInterceptors, TargetSource targetSource) {

if (this.beanFactory instanceof ConfigurableListableBeanFactory) {
AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass);
}

ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.copyFrom(this);

if (!proxyFactory.isProxyTargetClass()) {
if (shouldProxyTargetClass(beanClass, beanName)) {
proxyFactory.setProxyTargetClass(true);
}
else {
evaluateProxyInterfaces(beanClass, proxyFactory);
}
}

Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
proxyFactory.addAdvisors(advisors);
proxyFactory.setTargetSource(targetSource);
customizeProxyFactory(proxyFactory);

proxyFactory.setFrozen(this.freezeProxy);
if (advisorsPreFiltered()) {
proxyFactory.setPreFiltered(true);
}

return proxyFactory.getProxy(getProxyClassLoader());
}

进入 getProxy 方法,通过配置来指定配置工厂(jdk代理cglib),然后调用两个工厂都有的 getProxy 来获取代理实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public Object getProxy(@Nullable ClassLoader classLoader) {
return createAopProxy().getProxy(classLoader);
}
protected final synchronized AopProxy createAopProxy() {
if (!this.active) {
activate();
}
return getAopProxyFactory().createAopProxy(this);
}
// DefaultAopProxyFactory.java
@Override
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
if (config.isOptimize() config.isProxyTargetClass() hasNoUserSuppliedProxyInterfaces(config)) {
Class<?> targetClass = config.getTargetClass();
if (targetClass == null) {
throw new AopConfigException("TargetSource cannot determine target class: " +
"Either an interface or a target is required for proxy creation.");
}
if (targetClass.isInterface() Proxy.isProxyClass(targetClass)) {
return new JdkDynamicAopProxy(config);
}
// 虽然我的类是带有接口的,但是来到这里,使用cglib进行创建
return new ObjenesisCglibAopProxy(config);
}
else {
return new JdkDynamicAopProxy(config);
}
}

SpringBoot代理模式

上面跑出来的小问题,为啥带有接口还是使用了 cglib 创建,跟我之前读 Spring-AOP 的时候是不一样的结果。 这里就要说到 SpringBoot 的自动创建配置了:

1
2
3
4
5
6
7
// META-INF/additional-spring-configuration-metadata.json
{
"name": "spring.aop.proxy-target-class",
"type": "java.lang.Boolean",
"description": "Whether subclass-based (CGLIB) proxies are to be created (true), as opposed to standard Java interface-based proxies (false).",
"defaultValue": true
},

然而搜遍谷歌 Spring 项目,只看到 Spring 成员说了一句:

准备个需求

之前读其他无关数据库源码的时候,只是简单的依赖了 spring-boot-starter-web,现在由于需要数据库的参与,所以需要加上 MySQL 的驱动,以及一个最简单的 jdbc 框架。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>

application.yml 配置数据库连接信息。

1
2
3
4
5
spring:
datasource:
url: jdbc:mysql://192.168.1.152:3306/spring_trans?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true
username: root
password: root

一个接收需要插入数据库的请求:

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
public class UserController {

@Autowired
private UserService userService;

@PostMapping("users")
public void add(@RequestParam("name") String name, @RequestParam("orgName") String orgName) {
userService.add(name, orgName);
}

}

由于示例,就使用最简单的例子,插入一个用户以及他所属的组织机构,一对一的关系。使用 JdbcTemplate 直接插入 SQL 语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public interface UserService {
void add(String name, String orgName);
}
@Service
public class UserServiceImpl implements UserService {

@Autowired
private JdbcTemplate jdbcTemplate;

@Override
@Transactional
public void add(String name, String orgName) {
String userId = UUID.randomUUID().toString();
jdbcTemplate.update("insert into user_info(user_uuid, user_name)\n" +
"values (?,?);", userId, name);
// 使用一个RuntimeEx来触发事务回滚
int i = 1 / 0;
String orgUUID = UUID.randomUUID().toString();
jdbcTemplate.update("insert into org_info(org_uuid, org_name, user_uuid)\n" +
"VALUES (?, ?, ?);", orgUUID, orgName, userId);
}

}

应用事务

  • 编程式注解:使用 @Transactional 修饰的类或者方法,具有事务特性。(SpringBoot 时代一般用这个)
  • 声明式事务:通过定义配置文件,需要定义一套基于公司内部的方法命名规范。如以下的配置,update 以及 insert 为名的方法,使用事务,不然使用只读事务。
1
2
3
4
5
6
7
8
9
10
11
<tx:advice id="advice" transaction-manager="transactionManager">  
<tx:attributes>
<tx:method name="update*" propagation="REQUIRED" read-only="false" rollback-for="java.lang.Exception"/>
<tx:method name="insert" propagation="REQUIRED" read-only="false"/>
</tx:attributes>
</tx:advice>

<aop:config>
<aop:pointcut id="testService" expression="execution (* com.liweidan.service.MyBatisService.*(..))"/>
<aop:advisor advice-ref="advice" pointcut-ref="testService"/>
</aop:config>

在上面例子中的 UserServiceImpl 是使用编程式事务处理的,当没有加上 @Transactional 时,在 int i = 1 / 0; 处发生异常时,上面已经插入的用户信息并不会被回滚,这就不符合我们日常的业务需求了。OK,从这里开始将要开始看看 Spring 是如何应用事务到我们的代码上的。

依赖结构

OK,简单看看我们依赖了 spring-boot-starter-jdbc 都依赖了些什么东西。从图中可以看到,spring-jdbcspring-tx 以及 HicariCP 都进来了,spring-jdbc 模块也还好,封装了 JdbcTemplate 等一些偏向于原生 JDBC 的操作,spring-tx 就是此次需要说的重点了,事务管理模块,主要管理 Spring 自己定义的一些事务模型。HicariCP 则是一个高性能的数据库连接池。

基于AOP

我记得很清楚的,《从零开始架构WEB系统》中说到,可以使用 AOP 方式切入项目,获取 Connection 保存在线程的 ThreadLocal 中,在调用插入更新的时候,取出来开启事务,方法执行结束后判定是否是正常执行,如果遇到异常了,就 roolBack 插入的数据,否则 commit。那我们现在大概可以带着这个思路来看 Spring 源码。

自动配置

上一篇我们说过,SpringBoot 会自动扫描导入 org.springframework.boot.autoconfigure 包下的所有的配置,这次,事务的自动配置是 org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration。因为导入项目已经有 PlatformTransactionManager 类,所以该配置将被自动执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// TransactionAutoConfiguration.java: 
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(PlatformTransactionManager.class)
@AutoConfigureAfter({ JtaAutoConfiguration.class, HibernateJpaAutoConfiguration.class,
DataSourceTransactionManagerAutoConfiguration.class, Neo4jDataAutoConfiguration.class })
@EnableConfigurationProperties(TransactionProperties.class)
public class TransactionAutoConfiguration {

@Bean
@ConditionalOnMissingBean
public TransactionManagerCustomizers platformTransactionManagerCustomizers(
ObjectProvider<PlatformTransactionManagerCustomizer<?>> customizers) {
return new TransactionManagerCustomizers(customizers.orderedStream().collect(Collectors.toList()));
}

@Bean
@ConditionalOnMissingBean
@ConditionalOnSingleCandidate(ReactiveTransactionManager.class)
public TransactionalOperator transactionalOperator(ReactiveTransactionManager transactionManager) {
return TransactionalOperator.create(transactionManager);
}

@Configuration(proxyBeanMethods = false)
@ConditionalOnSingleCandidate(PlatformTransactionManager.class)
public static class TransactionTemplateConfiguration {

@Bean
@ConditionalOnMissingBean(TransactionOperations.class)
public TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) {
return new TransactionTemplate(transactionManager);
}

}

@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(TransactionManager.class)
@ConditionalOnMissingBean(AbstractTransactionManagementConfiguration.class)
public static class EnableTransactionManagementConfiguration {

@Configuration(proxyBeanMethods = false)
// 重点是这个注解,导入了些东西
@EnableTransactionManagement(proxyTargetClass = false)
@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false",
matchIfMissing = false)
public static class JdkDynamicAutoProxyConfiguration {

}

@Configuration(proxyBeanMethods = false)
@EnableTransactionManagement(proxyTargetClass = true)
@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
matchIfMissing = true)
public static class CglibAutoProxyConfiguration {

}

}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// EnableTransactionManagement.java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
// 导入一个Selector
@Import(TransactionManagementConfigurationSelector.class)
public @interface EnableTransactionManagement {

/**
* Indicate whether subclass-based (CGLIB) proxies are to be created ({@code true}) as
* opposed to standard Java interface-based proxies ({@code false}). The default is
* {@code false}. <strong>Applicable only if {@link #mode()} is set to
* {@link AdviceMode#PROXY}</strong>.
* <p>Note that setting this attribute to {@code true} will affect <em>all</em>
* Spring-managed beans requiring proxying, not just those marked with
* {@code @Transactional}. For example, other beans marked with Spring's
* {@code @Async} annotation will be upgraded to subclass proxying at the same
* time. This approach has no negative impact in practice unless one is explicitly
* expecting one type of proxy vs another, e.g. in tests.
*/
boolean proxyTargetClass() default false;

/**
* Indicate how transactional advice should be applied.
* <p><b>The default is {@link AdviceMode#PROXY}.</b>
* Please note that proxy mode allows for interception of calls through the proxy
* only. Local calls within the same class cannot get intercepted that way; an
* {@link Transactional} annotation on such a method within a local call will be
* ignored since Spring's interceptor does not even kick in for such a runtime
* scenario. For a more advanced mode of interception, consider switching this to
* {@link AdviceMode#ASPECTJ}.
*/
AdviceMode mode() default AdviceMode.PROXY;

/**
* Indicate the ordering of the execution of the transaction advisor
* when multiple advices are applied at a specific joinpoint.
* <p>The default is {@link Ordered#LOWEST_PRECEDENCE}.
*/
int order() default Ordered.LOWEST_PRECEDENCE;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// TransactionManagementConfigurationSelector.java
public class TransactionManagementConfigurationSelector extends AdviceModeImportSelector<EnableTransactionManagement> {

/**
* Returns {@link ProxyTransactionManagementConfiguration} or
* {@code AspectJ(Jta)TransactionManagementConfiguration} for {@code PROXY}
* and {@code ASPECTJ} values of {@link EnableTransactionManagement#mode()},
* respectively.
*/
@Override
protected String[] selectImports(AdviceMode adviceMode) {
switch (adviceMode) {
case PROXY:
return new String[] {AutoProxyRegistrar.class.getName(),
ProxyTransactionManagementConfiguration.class.getName()};
case ASPECTJ:
return new String[] {determineTransactionAspectClass()};
default:
return null;
}
}

private String determineTransactionAspectClass() {
return (ClassUtils.isPresent("javax.transaction.Transactional", getClass().getClassLoader()) ?
TransactionManagementConfigUtils.JTA_TRANSACTION_ASPECT_CONFIGURATION_CLASS_NAME :
TransactionManagementConfigUtils.TRANSACTION_ASPECT_CONFIGURATION_CLASS_NAME);
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
Collection<SourceClass> importCandidates, boolean checkForCircularImports) {

if (importCandidates.isEmpty()) {
return;
}

if (checkForCircularImports && isChainedImportOnStack(configClass)) {
this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
}
else {
this.importStack.push(configClass);
try {
for (SourceClass candidate : importCandidates) {
if (candidate.isAssignable(ImportSelector.class)) {
// Candidate class is an ImportSelector -> delegate to it to determine imports
Class<?> candidateClass = candidate.loadClass();
ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class,
this.environment, this.resourceLoader, this.registry);
if (selector instanceof DeferredImportSelector) {
this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
}
else {
// 在导入其他配置类的时候会使用上面的 selectImports 函数导入配置类
// 然后重新解析配置
String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames);
processImports(configClass, currentSourceClass, importSourceClasses, false);
}
// .....
}
}
}
}

来到这里,配置类的东西就已经被注册到 BeanFactory 中去,那么据之前的 SpringAOP 的介绍,BeanFactory 会在每次初始化 Bean 的时候,调用 BeanPostProcessor#postProcessAfterInitialization 这个函数来创建真实 Bean 的代理对象。 然后这件事情就交给了 AOP模块InfrastructureAdvisorAutoProxyCreator 来实现包装代理模式。

篇幅关系我只放关键的代码片段,快速过一下前面的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
throws BeanCreationException {
// ......
Object exposedObject = bean;
try {
populateBean(beanName, mbd, instanceWrapper);
// 包装实际Bean
exposedObject = initializeBean(beanName, exposedObject, mbd);
}
catch (Throwable ex) {
if (ex instanceof BeanCreationException && beanName.equals(((BeanCreationException) ex).getBeanName())) {
throw (BeanCreationException) ex;
}
else {
throw new BeanCreationException(
mbd.getResourceDescription(), beanName, "Initialization of bean failed", ex);
}
}
// ......
}
}
// initializeBean
protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) {
if (System.getSecurityManager() != null) {
AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
invokeAwareMethods(beanName, bean);
return null;
}, getAccessControlContext());
}
else {
invokeAwareMethods(beanName, bean);
}

Object wrappedBean = bean;
if (mbd == null !mbd.isSynthetic()) {
wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
}

try {
invokeInitMethods(beanName, wrappedBean, mbd);
}
catch (Throwable ex) {
throw new BeanCreationException(
(mbd != null ? mbd.getResourceDescription() : null),
beanName, "Invocation of init method failed", ex);
}
if (mbd == null !mbd.isSynthetic()) {
// 创建完成,使用集成的后处理器处理对应的Bean
wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
}

return wrappedBean;
}
// applyBeanPostProcessorsAfterInitialization使用后处理器包装Bean
public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName)
throws BeansException {

Object result = existingBean;
for (BeanPostProcessor processor : getBeanPostProcessors()) {
// 轮询到InfrastructureAdvisorAutoProxyCreator或AnnotationAwareAspectJAutoProxyCreator进行代理处理
Object current = processor.postProcessAfterInitialization(result, beanName);
if (current == null) {
return result;
}
result = current;
}
return result;
}

注:使用 InfrastructureAdvisorAutoProxyCreator 还是 AnnotationAwareAspectJAutoProxyCreator 取决于当前项目有没有依赖 Spring-AOP 模块,如果依赖 Spring-AOP 模块则会交给 AnnotationAwareAspectJAutoProxyCreator 进行处理。 两者的区别是:InfrastructureAdvisorAutoProxyCreator 不负责织入用户自定义的 AOP 类

然后,AbstractAdvisorAutoProxyCreator (两者的共同父类) 会找到所有的 Advisor 实现来织入这个实际对象的方法中。 因为在解析配置类的时候,BeanFactoryTransactionAttributeSourceAdvisor 这个类已经被注册到 BeanFactoryBeanDefinitionMap 中,所以第一个 Bean(大概率都是 Spring 自己需要的 Bean)创建的时候,就会触发上面自动配置中,创建 BeanFactoryTransactionAttributeSourceAdvisor 的配置类,调用方法进行相对应的创建。 然后又走了一遍 getBean 啊,doCreateBean 啊….创建 BeanFactoryTransactionAttributeSourceAdvisor。 好了,走完创建 BeanFactoryTransactionAttributeSourceAdvisor 的过程。我们现在需要看看怎么被织入。

织入方法

至于判定的方法,之前已经说过基于 @PointCut 切入,那么这个解析方式,应该很容易想到了吧,就是 类注解 + 方法级别注解 判定是否要切入当前方法。 上面两个织入类,无论怎么样都会来到这个函数织入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
return bean;
}
if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
return bean;
}
if (isInfrastructureClass(bean.getClass()) shouldSkip(bean.getClass(), beanName)) {
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}

// 获取当前Bean所需要的AOP拦截器链
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
if (specificInterceptors != DO_NOT_PROXY) {
this.advisedBeans.put(cacheKey, Boolean.TRUE);
// 进入创建代理对象
Object proxy = createProxy(
bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
this.proxyTypes.put(cacheKey, proxy.getClass());
return proxy;
}

this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}

进入配置并使用 ProxyFactory 来创建代理对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
@Nullable Object[] specificInterceptors, TargetSource targetSource) {

if (this.beanFactory instanceof ConfigurableListableBeanFactory) {
AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass);
}

ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.copyFrom(this);

if (!proxyFactory.isProxyTargetClass()) {
if (shouldProxyTargetClass(beanClass, beanName)) {
proxyFactory.setProxyTargetClass(true);
}
else {
evaluateProxyInterfaces(beanClass, proxyFactory);
}
}

Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
proxyFactory.addAdvisors(advisors);
proxyFactory.setTargetSource(targetSource);
customizeProxyFactory(proxyFactory);

proxyFactory.setFrozen(this.freezeProxy);
if (advisorsPreFiltered()) {
proxyFactory.setPreFiltered(true);
}

return proxyFactory.getProxy(getProxyClassLoader());
}

进入 getProxy 方法,通过配置来指定配置工厂(jdk代理cglib),然后调用两个工厂都有的 getProxy 来获取代理实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public Object getProxy(@Nullable ClassLoader classLoader) {
return createAopProxy().getProxy(classLoader);
}
protected final synchronized AopProxy createAopProxy() {
if (!this.active) {
activate();
}
return getAopProxyFactory().createAopProxy(this);
}
// DefaultAopProxyFactory.java
@Override
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
if (config.isOptimize() config.isProxyTargetClass() hasNoUserSuppliedProxyInterfaces(config)) {
Class<?> targetClass = config.getTargetClass();
if (targetClass == null) {
throw new AopConfigException("TargetSource cannot determine target class: " +
"Either an interface or a target is required for proxy creation.");
}
if (targetClass.isInterface() Proxy.isProxyClass(targetClass)) {
return new JdkDynamicAopProxy(config);
}
// 虽然我的类是带有接口的,但是来到这里,使用cglib进行创建
return new ObjenesisCglibAopProxy(config);
}
else {
return new JdkDynamicAopProxy(config);
}
}

SpringBoot代理模式

上面跑出来的小问题,为啥带有接口还是使用了 cglib 创建,跟我之前读 Spring-AOP 的时候是不一样的结果。 这里就要说到 SpringBoot 的自动创建配置了:

1
2
3
4
5
6
7
// META-INF/additional-spring-configuration-metadata.json
{
"name": "spring.aop.proxy-target-class",
"type": "java.lang.Boolean",
"description": "Whether subclass-based (CGLIB) proxies are to be created (true), as opposed to standard Java interface-based proxies (false).",
"defaultValue": true
},

然而搜遍谷歌 Spring 项目,只看到 Spring 成员说了一句:

意思是 Cglib 代理能够减少类转换异常。

织入拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Override
public Object getProxy(@Nullable ClassLoader classLoader) {
// 省略部分代码
// Configure CGLIB Enhancer...
Enhancer enhancer = createEnhancer();
if (classLoader != null) {
enhancer.setClassLoader(classLoader);
if (classLoader instanceof SmartClassLoader &&
((SmartClassLoader) classLoader).isClassReloadable(proxySuperClass)) {
enhancer.setUseCache(false);
}
}
enhancer.setSuperclass(proxySuperClass);
enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised));
enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
enhancer.setStrategy(new ClassLoaderAwareGeneratorStrategy(classLoader));

// 在Callback这里即会织入BeanFactoryTransactionAttributeSourceAdvisor中的拦截器:
// TransactionInterceptor
Callback[] callbacks = getCallbacks(rootClass);
Class<?>[] types = new Class<?>[callbacks.length];
for (int x = 0; x < types.length; x++) {
types[x] = callbacks[x].getClass();
}
// fixedInterceptorMap only populated at this point, after getCallbacks call above
enhancer.setCallbackFilter(new ProxyCallbackFilter(
this.advised.getConfigurationOnlyCopy(), this.fixedInterceptorMap, this.fixedInterceptorOffset));
enhancer.setCallbackTypes(types);

// Generate the proxy class and create a proxy instance.
return createProxyClassAndInstance(enhancer, callbacks);
}
catch (CodeGenerationException IllegalArgumentException ex) {
throw new AopConfigException("Could not generate CGLIB subclass of " + this.advised.getTargetClass() +
": Common causes of this problem include using a final class or a non-visible class",
ex);
}
catch (Throwable ex) {
// TargetSource.getTarget() failed
throw new AopConfigException("Unexpected AOP exception", ex);
}
}

拦截器织入完成接下来就需要看看怎么被调用的了。


插入事务

真男人要直接,直接在 Controller 打断点,进入业务方法之前,会先进入 TransactionInterceptor#invoke

1
2
3
4
5
6
7
8
9
@Override
@Nullable
public Object invoke(MethodInvocation invocation) throws Throwable {
// 获取目标类
Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);

// 开启事务执行方法
return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
}

呃,代码还是挺长的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
@Nullable
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
final InvocationCallback invocation) throws Throwable {

// 获取所有配置的事务属性元数据类,如果为null,代表默认事务(默认是autoCommit=true)
TransactionAttributeSource tas = getTransactionAttributeSource();
// 从缓存中命中当前方法的事务配置
final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
// 获取事务管理器
final TransactionManager tm = determineTransactionManager(txAttr);

// Reactive环境走这里,先跳过
if (this.reactiveAdapterRegistry != null && tm instanceof ReactiveTransactionManager) {
ReactiveTransactionSupport txSupport = this.transactionSupportCache.computeIfAbsent(method, key -> {
if (KotlinDetector.isKotlinType(method.getDeclaringClass()) && KotlinDelegate.isSuspend(method)) {
throw new TransactionUsageException(
"Unsupported annotated transaction on suspending function detected: " + method +
". Use TransactionalOperator.transactional extensions instead.");
}
ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(method.getReturnType());
if (adapter == null) {
throw new IllegalStateException("Cannot apply reactive transaction to non-reactive return type: " +
method.getReturnType());
}
return new ReactiveTransactionSupport(adapter);
});
return txSupport.invokeWithinTransaction(
method, targetClass, invocation, txAttr, (ReactiveTransactionManager) tm);
}

// 获取Spring提供的统一事务管理器
PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);

if (txAttr == null !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
// 创建或者加入当前事务
TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);

Object retVal;
try {
// 执行目标方法,如果有下一个AOP链则接下去执行
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// 遇到异常,回滚数据库事务
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
cleanupTransactionInfo(txInfo);
}

// 响应式数据库操作 也是先跳过
if (vavrPresent && VavrDelegate.isVavrTry(retVal)) {
// Set rollback-only in case of Vavr failure matching our rollback rules...
TransactionStatus status = txInfo.getTransactionStatus();
if (status != null && txAttr != null) {
retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
}
}

// 提交数据库事务
commitTransactionAfterReturning(txInfo);
return retVal;
}
// CallbackPreferringPlatformTransactionManager 可回调的管理器
// 也是响应式的内容
else {
final ThrowableHolder throwableHolder = new ThrowableHolder();

// It's a CallbackPreferringPlatformTransactionManager: pass a TransactionCallback in.
try {
Object result = ((CallbackPreferringPlatformTransactionManager) ptm).execute(txAttr, status -> {
TransactionInfo txInfo = prepareTransactionInfo(ptm, txAttr, joinpointIdentification, status);
try {
Object retVal = invocation.proceedWithInvocation();
if (vavrPresent && VavrDelegate.isVavrTry(retVal)) {
// Set rollback-only in case of Vavr failure matching our rollback rules...
retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
}
return retVal;
}
catch (Throwable ex) {
if (txAttr.rollbackOn(ex)) {
// A RuntimeException: will lead to a rollback.
if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}
else {
throw new ThrowableHolderException(ex);
}
}
else {
// A normal return value: will lead to a commit.
throwableHolder.throwable = ex;
return null;
}
}
finally {
cleanupTransactionInfo(txInfo);
}
});

// Check result state: It might indicate a Throwable to rethrow.
if (throwableHolder.throwable != null) {
throw throwableHolder.throwable;
}
return result;
}
catch (ThrowableHolderException ex) {
throw ex.getCause();
}
catch (TransactionSystemException ex2) {
if (throwableHolder.throwable != null) {
logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
ex2.initApplicationException(throwableHolder.throwable);
}
throw ex2;
}
catch (Throwable ex2) {
if (throwableHolder.throwable != null) {
logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
}
throw ex2;
}
}
}

那下面就一步一步来看。

TransactionAttribute事务属性

这是一个定义了事务级别以及其他比如超时信息的事务元信息类,当前获取的是一个 RuleBasedTransactionAttribute 基于一定规则的事务属性,定义了必须在抛出 Runtime 异常的时候回滚数据库。 父级是 TransactionDefinition,这可是一个元老级别的类了,定义了事务管理器常见所需的隔离级别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
public interface TransactionDefinition {

// ------------------------------ Spring事务隔离级别 ------------------------------
// 如果当前没有事务,则创建一个新事务
int PROPAGATION_REQUIRED = 0;

// 如果当前没有事务则以无事务的方式运行
int PROPAGATION_SUPPORTS = 1;

// 当前有事务就加入没有就抛异常
int PROPAGATION_MANDATORY = 2;

// 阻塞当前事务,创建一个子事务
int PROPAGATION_REQUIRES_NEW = 3;

// 一直以无事务的状态运行
int PROPAGATION_NOT_SUPPORTED = 4;

// 如果当前有事务则抛出异常
int PROPAGATION_NEVER = 5;

// 如果存在事务则在嵌套事务中运行
// 行为类似于 PROPAGATION_REQUIRED
int PROPAGATION_NESTED = 6;

// ------------------------------ jdbc原生事务隔离级别 ------------------------------

// 使用数据库默认的级别
int ISOLATION_DEFAULT = -1;

// 未提交读
int ISOLATION_READ_UNCOMMITTED = 1; // same as java.sql.Connection.TRANSACTION_READ_UNCOMMITTED;

// 不可重复读
int ISOLATION_READ_COMMITTED = 2; // same as java.sql.Connection.TRANSACTION_READ_COMMITTED;


// 可重复读
int ISOLATION_REPEATABLE_READ = 4; // same as java.sql.Connection.TRANSACTION_REPEATABLE_READ;

// 串行执行事务
int ISOLATION_SERIALIZABLE = 8; // same as java.sql.Connection.TRANSACTION_SERIALIZABLE;


/**
* Use the default timeout of the underlying transaction system,
* or none if timeouts are not supported.
*/
int TIMEOUT_DEFAULT = -1;


/**
* Return the propagation behavior.
* <p>Must return one of the {@code PROPAGATION_XXX} constants
* defined on {@link TransactionDefinition this interface}.
* <p>The default is {@link #PROPAGATION_REQUIRED}.
* @return the propagation behavior
* @see #PROPAGATION_REQUIRED
* @see org.springframework.transaction.support.TransactionSynchronizationManager#isActualTransactionActive()
*/
default int getPropagationBehavior() {
return PROPAGATION_REQUIRED;
}

/**
* Return the isolation level.
* <p>Must return one of the {@code ISOLATION_XXX} constants defined on
* {@link TransactionDefinition this interface}. Those constants are designed
* to match the values of the same constants on {@link java.sql.Connection}.
* <p>Exclusively designed for use with {@link #PROPAGATION_REQUIRED} or
* {@link #PROPAGATION_REQUIRES_NEW} since it only applies to newly started
* transactions. Consider switching the "validateExistingTransactions" flag to
* "true" on your transaction manager if you'd like isolation level declarations
* to get rejected when participating in an existing transaction with a different
* isolation level.
* <p>The default is {@link #ISOLATION_DEFAULT}. Note that a transaction manager
* that does not support custom isolation levels will throw an exception when
* given any other level than {@link #ISOLATION_DEFAULT}.
* @return the isolation level
* @see #ISOLATION_DEFAULT
* @see org.springframework.transaction.support.AbstractPlatformTransactionManager#setValidateExistingTransaction
*/
default int getIsolationLevel() {
return ISOLATION_DEFAULT;
}

/**
* Return the transaction timeout.
* <p>Must return a number of seconds, or {@link #TIMEOUT_DEFAULT}.
* <p>Exclusively designed for use with {@link #PROPAGATION_REQUIRED} or
* {@link #PROPAGATION_REQUIRES_NEW} since it only applies to newly started
* transactions.
* <p>Note that a transaction manager that does not support timeouts will throw
* an exception when given any other timeout than {@link #TIMEOUT_DEFAULT}.
* <p>The default is {@link #TIMEOUT_DEFAULT}.
* @return the transaction timeout
*/
default int getTimeout() {
return TIMEOUT_DEFAULT;
}

/**
* Return whether to optimize as a read-only transaction.
* <p>The read-only flag applies to any transaction context, whether backed
* by an actual resource transaction ({@link #PROPAGATION_REQUIRED}/
* {@link #PROPAGATION_REQUIRES_NEW}) or operating non-transactionally at
* the resource level ({@link #PROPAGATION_SUPPORTS}). In the latter case,
* the flag will only apply to managed resources within the application,
* such as a Hibernate {@code Session}.
* <p>This just serves as a hint for the actual transaction subsystem;
* it will <i>not necessarily</i> cause failure of write access attempts.
* A transaction manager which cannot interpret the read-only hint will
* <i>not</i> throw an exception when asked for a read-only transaction.
* @return {@code true} if the transaction is to be optimized as read-only
* ({@code false} by default)
* @see org.springframework.transaction.support.TransactionSynchronization#beforeCommit(boolean)
* @see org.springframework.transaction.support.TransactionSynchronizationManager#isCurrentTransactionReadOnly()
*/
default boolean isReadOnly() {
return false;
}

/**
* Return the name of this transaction. Can be {@code null}.
* <p>This will be used as the transaction name to be shown in a
* transaction monitor, if applicable (for example, WebLogic's).
* <p>In case of Spring's declarative transactions, the exposed name will be
* the {@code fully-qualified class name + "." + method name} (by default).
* @return the name of this transaction ({@code null} by default}
* @see org.springframework.transaction.interceptor.TransactionAspectSupport
* @see org.springframework.transaction.support.TransactionSynchronizationManager#getCurrentTransactionName()
*/
@Nullable
default String getName() {
return null;
}


// Static builder methods

/**
* Return an unmodifiable {@code TransactionDefinition} with defaults.
* <p>For customization purposes, use the modifiable
* {@link org.springframework.transaction.support.DefaultTransactionDefinition}
* instead.
* @since 5.2
*/
static TransactionDefinition withDefaults() {
return StaticTransactionDefinition.INSTANCE;
}

}

那这个东西用在哪里呢,后面就会看到处理方式了,先放一放。

事务管理器

这一步没做什么,就当是单纯获得了一个事务管理器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Nullable
protected TransactionManager determineTransactionManager(@Nullable TransactionAttribute txAttr) {
// Do not attempt to lookup tx manager if no tx attributes are set
if (txAttr == null this.beanFactory == null) {
return getTransactionManager();
}

String qualifier = txAttr.getQualifier();
if (StringUtils.hasText(qualifier)) {
return determineQualifiedTransactionManager(this.beanFactory, qualifier);
}
else if (StringUtils.hasText(this.transactionManagerBeanName)) {
return determineQualifiedTransactionManager(this.beanFactory, this.transactionManagerBeanName);
}
else {
TransactionManager defaultTransactionManager = getTransactionManager();
if (defaultTransactionManager == null) {
defaultTransactionManager = this.transactionManagerCache.get(DEFAULT_TRANSACTION_MANAGER_KEY);
if (defaultTransactionManager == null) {
defaultTransactionManager = this.beanFactory.getBean(TransactionManager.class);
this.transactionManagerCache.putIfAbsent(
DEFAULT_TRANSACTION_MANAGER_KEY, defaultTransactionManager);
}
}
return defaultTransactionManager;
}
}

开启数据库事务

可以看到,传递了一个事务定义元数据来创建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
protected TransactionInfo createTransactionIfNecessary(@Nullable PlatformTransactionManager tm,
@Nullable TransactionAttribute txAttr, final String joinpointIdentification) {

// 处理名字
if (txAttr != null && txAttr.getName() == null) {
txAttr = new DelegatingTransactionAttribute(txAttr) {
@Override
public String getName() {
return joinpointIdentification;
}
};
}

TransactionStatus status = null;
if (txAttr != null) {
if (tm != null) {
status = tm.getTransaction(txAttr);
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Skipping transactional joinpoint [" + joinpointIdentification +
"] because no transaction manager has been configured");
}
}
}
// 管理当前开启的事务管理器
return prepareTransactionInfo(tm, txAttr, joinpointIdentification, status);
}

首先,先根据当前的事务管理级别创建 TransactionStatus

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
@Override
public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
throws TransactionException {

// 获取 TransactionDefinition,如果配置中没有定义则获取默认的事务定义.
TransactionDefinition def = (definition != null ? definition : TransactionDefinition.withDefaults());

// 获取事务状态对象
Object transaction = doGetTransaction();
boolean debugEnabled = logger.isDebugEnabled();

if (isExistingTransaction(transaction)) {
// 当前存在事务,根据不同等级返回 TransactionStatus.
return handleExistingTransaction(def, transaction, debugEnabled);
}

// 超时的话抛出异常.
if (def.getTimeout() < TransactionDefinition.TIMEOUT_DEFAULT) {
throw new InvalidTimeoutException("Invalid transaction timeout", def.getTimeout());
}

// 不存在事务的时候,需要根据上面的事务定义来继续行为.
if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) {
// 这个等级是没有事务即抛出异常
throw new IllegalTransactionStateException(
"No existing transaction found for transaction marked with propagation 'mandatory'");
}
else if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED
def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW
def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
SuspendedResourcesHolder suspendedResources = suspend(null);
if (debugEnabled) {
logger.debug("Creating new transaction with name [" + def.getName() + "]: " + def);
}
try {
// 创建新的 TransactionStatus 然后开启数据库事务
boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
// 创建一个新的DefaultTransactionStatus贯穿整个事务
DefaultTransactionStatus status = newTransactionStatus(
def, transaction, true, newSynchronization, debugEnabled, suspendedResources);
// 开启新事务
doBegin(transaction, def);
// 设置激活程序中的事务管理器,设置只读属性、事务名字、隔离级别等信息
prepareSynchronization(status, def);
return status;
}
catch (RuntimeException Error ex) {
resume(null, suspendedResources);
throw ex;
}
}
else {
// Create "empty" transaction: no actual transaction, but potentially synchronization.
if (def.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT && logger.isWarnEnabled()) {
logger.warn("Custom isolation level specified but no actual transaction initiated; " +
"isolation level will effectively be ignored: " + def);
}
boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
return prepareTransactionStatus(def, null, true, newSynchronization, debugEnabled, null);
}
}

OK,接下来看看 doBegin 怎么管理 Connection

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// 开启事务管理器
@Override
protected void doBegin(Object transaction, TransactionDefinition definition) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
Connection con = null;

try {
if (!txObject.hasConnectionHolder()
txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
// 通过数据库连接池拿到 Connection
Connection newCon = obtainDataSource().getConnection();
if (logger.isDebugEnabled()) {
logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
}
// 使用ConnectionHolder管理当前的连接
txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
}

txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
con = txObject.getConnectionHolder().getConnection();

// Connection属性设置(之前的隔离级别、是否只读)
Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
txObject.setPreviousIsolationLevel(previousIsolationLevel);
txObject.setReadOnly(definition.isReadOnly());

// 修改Connection的只读状态
if (con.getAutoCommit()) {
txObject.setMustRestoreAutoCommit(true);
if (logger.isDebugEnabled()) {
logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
}
con.setAutoCommit(false);
}

// 如果当前是只读事务则发送 SET TRANSACTION READ ONLY 给数据库切换事务状态
prepareTransactionalConnection(con, definition);
txObject.getConnectionHolder().setTransactionActive(true);

int timeout = determineTimeout(definition);
if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
}

// 把ConnectionHolder绑定到TransactionSynchronizationManager
if (txObject.isNewConnectionHolder()) {
TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());
}
}

catch (Throwable ex) {
if (txObject.isNewConnectionHolder()) {
DataSourceUtils.releaseConnection(con, obtainDataSource());
txObject.setConnectionHolder(null, false);
}
throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex);
}
}

开始执行

为了防止需要拖拉到上面去阅读,我先把之前执行到哪个地方放在这里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
@Nullable
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
final InvocationCallback invocation) throws Throwable {

// 获取所有配置的事务属性元数据类,如果为null,代表默认事务(默认是autoCommit=true)
TransactionAttributeSource tas = getTransactionAttributeSource();
// 从缓存中命中当前方法的事务配置
final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
// 获取事务管理器
final TransactionManager tm = determineTransactionManager(txAttr);

// Reactive环境走这里,先跳过
if (this.reactiveAdapterRegistry != null && tm instanceof ReactiveTransactionManager) {
ReactiveTransactionSupport txSupport = this.transactionSupportCache.computeIfAbsent(method, key -> {
if (KotlinDetector.isKotlinType(method.getDeclaringClass()) && KotlinDelegate.isSuspend(method)) {
throw new TransactionUsageException(
"Unsupported annotated transaction on suspending function detected: " + method +
". Use TransactionalOperator.transactional extensions instead.");
}
ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(method.getReturnType());
if (adapter == null) {
throw new IllegalStateException("Cannot apply reactive transaction to non-reactive return type: " +
method.getReturnType());
}
return new ReactiveTransactionSupport(adapter);
});
return txSupport.invokeWithinTransaction(
method, targetClass, invocation, txAttr, (ReactiveTransactionManager) tm);
}

// 获取Spring提供的统一事务管理器
PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);

if (txAttr == null !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
// 创建或者加入当前事务
TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);

Object retVal;
try {
// 执行目标方法,如果有下一个AOP链则接下去执行
// ---------------> 已经准备好了事务信息,可以开始执行整个AOP链条了
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// 遇到异常,回滚数据库事务
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
// 清空当前事务,如果当前是嵌套事务,管理器会重新拿到之前的事务
cleanupTransactionInfo(txInfo);
}

// 响应式数据库操作 也是先跳过
if (vavrPresent && VavrDelegate.isVavrTry(retVal)) {
// Set rollback-only in case of Vavr failure matching our rollback rules...
TransactionStatus status = txInfo.getTransactionStatus();
if (status != null && txAttr != null) {
retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
}
}

// 提交数据库事务
commitTransactionAfterReturning(txInfo);
return retVal;
}
// CallbackPreferringPlatformTransactionManager 可回调的管理器
// 也是响应式的内容
else {
// 省略响应式内容.........
}
}

正常提交

拿到事务管理器,提交事务:

1
2
3
4
5
6
7
8
protected void commitTransactionAfterReturning(@Nullable TransactionInfo txInfo) {
if (txInfo != null && txInfo.getTransactionStatus() != null) {
if (logger.isTraceEnabled()) {
logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() + "]");
}
txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
}
}

需要经过判断来做是否真正提交:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Override
public final void commit(TransactionStatus status) throws TransactionException {
if (status.isCompleted()) {
throw new IllegalTransactionStateException(
"Transaction is already completed - do not call commit or rollback more than once per transaction");
}

DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;

// 根据用户TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();设置
// 是否不让提交(比如校验没有通过,但是没有抛出异常) 如果设置了则回滚数据库
if (defStatus.isLocalRollbackOnly()) {
if (defStatus.isDebug()) {
logger.debug("Transactional code has requested rollback");
}
processRollback(defStatus, false);
return;
}

// 这里是判断全局事务中配置了只能在全局回滚并且当前已经出现错误,如果当前是内部事务,会标记业务层事务出现错误
if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) {
if (defStatus.isDebug()) {
logger.debug("Global transaction is marked as rollback-only but transactional code requested commit");
}
processRollback(defStatus, true);
return;
}

// 处理提交事务
processCommit(defStatus);
}

处理提交,可以说,我们项目中使用到提交事务生命周期都在这里被调用到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
private void processCommit(DefaultTransactionStatus status) throws TransactionException {
try {
boolean beforeCompletionInvoked = false;

try {
boolean unexpectedRollback = false;
// 下面三个是调用相对应的生命周期
prepareForCommit(status);
triggerBeforeCommit(status);
triggerBeforeCompletion(status);
beforeCompletionInvoked = true;

// SavePoint 先不看
if (status.hasSavepoint()) {
if (status.isDebug()) {
logger.debug("Releasing transaction savepoint");
}
unexpectedRollback = status.isGlobalRollbackOnly();
status.releaseHeldSavepoint();
}
// 当前是一个新的事务
else if (status.isNewTransaction()) {
if (status.isDebug()) {
logger.debug("Initiating transaction commit");
}
unexpectedRollback = status.isGlobalRollbackOnly();
// 真正做提交的地方
doCommit(status);
}
else if (isFailEarlyOnGlobalRollbackOnly()) {
unexpectedRollback = status.isGlobalRollbackOnly();
}

// Throw UnexpectedRollbackException if we have a global rollback-only
// marker but still didn't get a corresponding exception from commit.
if (unexpectedRollback) {
throw new UnexpectedRollbackException(
"Transaction silently rolled back because it has been marked as rollback-only");
}
}
catch (UnexpectedRollbackException ex) {
// can only be caused by doCommit
triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK);
throw ex;
}
catch (TransactionException ex) {
// can only be caused by doCommit
if (isRollbackOnCommitFailure()) {
doRollbackOnCommitException(status, ex);
}
else {
triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN);
}
throw ex;
}
catch (RuntimeException Error ex) {
if (!beforeCompletionInvoked) {
triggerBeforeCompletion(status);
}
doRollbackOnCommitException(status, ex);
throw ex;
}

// Trigger afterCommit callbacks, with an exception thrown there
// propagated to callers but the transaction still considered as committed.
try {
// 调用事务生命周期的后处理器
triggerAfterCommit(status);
}
finally {
triggerAfterCompletion(status, TransactionSynchronization.STATUS_COMMITTED);
}

}
finally {
cleanupAfterCompletion(status);
}
}

拿到 Connection 调用 Commit 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
protected void doCommit(DefaultTransactionStatus status) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
Connection con = txObject.getConnectionHolder().getConnection();
if (status.isDebug()) {
logger.debug("Committing JDBC transaction on Connection [" + con + "]");
}
try {
con.commit();
}
catch (SQLException ex) {
throw new TransactionSystemException("Could not commit JDBC transaction", ex);
}
}

回滚数据库

业务代码加一个除以 0 的计算:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Service
public class UserServiceImpl implements UserService {

@Autowired
private JdbcTemplate jdbcTemplate;

@Override
@Transactional
public void add(String name, String orgName) {
String userId = UUID.randomUUID().toString();
jdbcTemplate.update("insert into user_info(user_uuid, user_name)\n" +
"values (?,?);", userId, name);
int i = 1 / 0;
String orgUUID = UUID.randomUUID().toString();
jdbcTemplate.update("insert into org_info(org_uuid, org_name, user_uuid)\n" +
"VALUES (?, ?, ?);", orgUUID, orgName, userId);
}

}

那么出现了异常了,这一步会来到 try-catch 中的 catch:

1
2
3
4
5
6
7
8
9
10
11
12
try {
// 执行目标方法,如果有下一个AOP链则接下去执行
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// 遇到异常,回滚数据库事务
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
cleanupTransactionInfo(txInfo);
}

这个是简单了很多了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
if (txInfo != null && txInfo.getTransactionStatus() != null) {
if (logger.isTraceEnabled()) {
logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() +
"] after exception: " + ex);
}
// 判断当前抛出的异常是否需要回滚数据库
if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
try {
txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
}
catch (TransactionSystemException ex2) {
logger.error("Application exception overridden by rollback exception", ex);
ex2.initApplicationException(ex);
throw ex2;
}
catch (RuntimeException Error ex2) {
logger.error("Application exception overridden by rollback exception", ex);
throw ex2;
}
}
// 如果不会滚,继续提交
else {
// We don't roll back on this exception.
// Will still roll back if TransactionStatus.isRollbackOnly() is true.
try {
txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
}
catch (TransactionSystemException ex2) {
logger.error("Application exception overridden by commit exception", ex);
ex2.initApplicationException(ex);
throw ex2;
}
catch (RuntimeException Error ex2) {
logger.error("Application exception overridden by commit exception", ex);
throw ex2;
}
}
}
}

好吧来到了处理方法:

1
2
3
4
5
6
7
8
9
10
@Override
public final void rollback(TransactionStatus status) throws TransactionException {
if (status.isCompleted()) {
throw new IllegalTransactionStateException(
"Transaction is already completed - do not call commit or rollback more than once per transaction");
}

DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;
processRollback(defStatus, false);
}

那么其实跟提交的套路是一样的,触发一系列的生命周期:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
private void processRollback(DefaultTransactionStatus status, boolean unexpected) {
try {
boolean unexpectedRollback = unexpected;

try {
triggerBeforeCompletion(status);

if (status.hasSavepoint()) {
if (status.isDebug()) {
logger.debug("Rolling back transaction to savepoint");
}
status.rollbackToHeldSavepoint();
}
else if (status.isNewTransaction()) {
if (status.isDebug()) {
logger.debug("Initiating transaction rollback");
}
doRollback(status);
}
else {
// Participating in larger transaction
if (status.hasTransaction()) {
if (status.isLocalRollbackOnly() isGlobalRollbackOnParticipationFailure()) {
if (status.isDebug()) {
logger.debug("Participating transaction failed - marking existing transaction as rollback-only");
}
doSetRollbackOnly(status);
}
else {
if (status.isDebug()) {
logger.debug("Participating transaction failed - letting transaction originator decide on rollback");
}
}
}
else {
logger.debug("Should roll back transaction but cannot - no transaction available");
}
// Unexpected rollback only matters here if we're asked to fail early
if (!isFailEarlyOnGlobalRollbackOnly()) {
unexpectedRollback = false;
}
}
}
catch (RuntimeException Error ex) {
triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN);
throw ex;
}

triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK);

// Raise UnexpectedRollbackException if we had a global rollback-only marker
if (unexpectedRollback) {
throw new UnexpectedRollbackException(
"Transaction rolled back because it has been marked as rollback-only");
}
}
finally {
cleanupAfterCompletion(status);
}
}

拿到 Connection 进行回滚。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
protected void doRollback(DefaultTransactionStatus status) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
Connection con = txObject.getConnectionHolder().getConnection();
if (status.isDebug()) {
logger.debug("Rolling back JDBC transaction on Connection [" + con + "]");
}
try {
con.rollback();
}
catch (SQLException ex) {
throw new TransactionSystemException("Could not roll back JDBC transaction", ex);
}
}

意思是 Cglib 代理能够减少类转换异常。

织入拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Override
public Object getProxy(@Nullable ClassLoader classLoader) {
// 省略部分代码
// Configure CGLIB Enhancer...
Enhancer enhancer = createEnhancer();
if (classLoader != null) {
enhancer.setClassLoader(classLoader);
if (classLoader instanceof SmartClassLoader &&
((SmartClassLoader) classLoader).isClassReloadable(proxySuperClass)) {
enhancer.setUseCache(false);
}
}
enhancer.setSuperclass(proxySuperClass);
enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised));
enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
enhancer.setStrategy(new ClassLoaderAwareGeneratorStrategy(classLoader));

// 在Callback这里即会织入BeanFactoryTransactionAttributeSourceAdvisor中的拦截器:
// TransactionInterceptor
Callback[] callbacks = getCallbacks(rootClass);
Class<?>[] types = new Class<?>[callbacks.length];
for (int x = 0; x < types.length; x++) {
types[x] = callbacks[x].getClass();
}
// fixedInterceptorMap only populated at this point, after getCallbacks call above
enhancer.setCallbackFilter(new ProxyCallbackFilter(
this.advised.getConfigurationOnlyCopy(), this.fixedInterceptorMap, this.fixedInterceptorOffset));
enhancer.setCallbackTypes(types);

// Generate the proxy class and create a proxy instance.
return createProxyClassAndInstance(enhancer, callbacks);
}
catch (CodeGenerationException IllegalArgumentException ex) {
throw new AopConfigException("Could not generate CGLIB subclass of " + this.advised.getTargetClass() +
": Common causes of this problem include using a final class or a non-visible class",
ex);
}
catch (Throwable ex) {
// TargetSource.getTarget() failed
throw new AopConfigException("Unexpected AOP exception", ex);
}
}

拦截器织入完成接下来就需要看看怎么被调用的了。


插入事务

真男人要直接,直接在 Controller 打断点,进入业务方法之前,会先进入 TransactionInterceptor#invoke

1
2
3
4
5
6
7
8
9
@Override
@Nullable
public Object invoke(MethodInvocation invocation) throws Throwable {
// 获取目标类
Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);

// 开启事务执行方法
return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
}

呃,代码还是挺长的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
@Nullable
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
final InvocationCallback invocation) throws Throwable {

// 获取所有配置的事务属性元数据类,如果为null,代表默认事务(默认是autoCommit=true)
TransactionAttributeSource tas = getTransactionAttributeSource();
// 从缓存中命中当前方法的事务配置
final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
// 获取事务管理器
final TransactionManager tm = determineTransactionManager(txAttr);

// Reactive环境走这里,先跳过
if (this.reactiveAdapterRegistry != null && tm instanceof ReactiveTransactionManager) {
ReactiveTransactionSupport txSupport = this.transactionSupportCache.computeIfAbsent(method, key -> {
if (KotlinDetector.isKotlinType(method.getDeclaringClass()) && KotlinDelegate.isSuspend(method)) {
throw new TransactionUsageException(
"Unsupported annotated transaction on suspending function detected: " + method +
". Use TransactionalOperator.transactional extensions instead.");
}
ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(method.getReturnType());
if (adapter == null) {
throw new IllegalStateException("Cannot apply reactive transaction to non-reactive return type: " +
method.getReturnType());
}
return new ReactiveTransactionSupport(adapter);
});
return txSupport.invokeWithinTransaction(
method, targetClass, invocation, txAttr, (ReactiveTransactionManager) tm);
}

// 获取Spring提供的统一事务管理器
PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);

if (txAttr == null !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
// 创建或者加入当前事务
TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);

Object retVal;
try {
// 执行目标方法,如果有下一个AOP链则接下去执行
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// 遇到异常,回滚数据库事务
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
cleanupTransactionInfo(txInfo);
}

// 响应式数据库操作 也是先跳过
if (vavrPresent && VavrDelegate.isVavrTry(retVal)) {
// Set rollback-only in case of Vavr failure matching our rollback rules...
TransactionStatus status = txInfo.getTransactionStatus();
if (status != null && txAttr != null) {
retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
}
}

// 提交数据库事务
commitTransactionAfterReturning(txInfo);
return retVal;
}
// CallbackPreferringPlatformTransactionManager 可回调的管理器
// 也是响应式的内容
else {
final ThrowableHolder throwableHolder = new ThrowableHolder();

// It's a CallbackPreferringPlatformTransactionManager: pass a TransactionCallback in.
try {
Object result = ((CallbackPreferringPlatformTransactionManager) ptm).execute(txAttr, status -> {
TransactionInfo txInfo = prepareTransactionInfo(ptm, txAttr, joinpointIdentification, status);
try {
Object retVal = invocation.proceedWithInvocation();
if (vavrPresent && VavrDelegate.isVavrTry(retVal)) {
// Set rollback-only in case of Vavr failure matching our rollback rules...
retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
}
return retVal;
}
catch (Throwable ex) {
if (txAttr.rollbackOn(ex)) {
// A RuntimeException: will lead to a rollback.
if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}
else {
throw new ThrowableHolderException(ex);
}
}
else {
// A normal return value: will lead to a commit.
throwableHolder.throwable = ex;
return null;
}
}
finally {
cleanupTransactionInfo(txInfo);
}
});

// Check result state: It might indicate a Throwable to rethrow.
if (throwableHolder.throwable != null) {
throw throwableHolder.throwable;
}
return result;
}
catch (ThrowableHolderException ex) {
throw ex.getCause();
}
catch (TransactionSystemException ex2) {
if (throwableHolder.throwable != null) {
logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
ex2.initApplicationException(throwableHolder.throwable);
}
throw ex2;
}
catch (Throwable ex2) {
if (throwableHolder.throwable != null) {
logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
}
throw ex2;
}
}
}

那下面就一步一步来看。

TransactionAttribute事务属性

这是一个定义了事务级别以及其他比如超时信息的事务元信息类,当前获取的是一个 RuleBasedTransactionAttribute 基于一定规则的事务属性,定义了必须在抛出 Runtime 异常的时候回滚数据库。 父级是 TransactionDefinition,这可是一个元老级别的类了,定义了事务管理器常见所需的隔离级别。

准备个需求

之前读其他无关数据库源码的时候,只是简单的依赖了 spring-boot-starter-web,现在由于需要数据库的参与,所以需要加上 MySQL 的驱动,以及一个最简单的 jdbc 框架。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>

application.yml 配置数据库连接信息。

1
2
3
4
5
spring:
datasource:
url: jdbc:mysql://192.168.1.152:3306/spring_trans?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true
username: root
password: root

一个接收需要插入数据库的请求:

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
public class UserController {

@Autowired
private UserService userService;

@PostMapping("users")
public void add(@RequestParam("name") String name, @RequestParam("orgName") String orgName) {
userService.add(name, orgName);
}

}

由于示例,就使用最简单的例子,插入一个用户以及他所属的组织机构,一对一的关系。使用 JdbcTemplate 直接插入 SQL 语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public interface UserService {
void add(String name, String orgName);
}
@Service
public class UserServiceImpl implements UserService {

@Autowired
private JdbcTemplate jdbcTemplate;

@Override
@Transactional
public void add(String name, String orgName) {
String userId = UUID.randomUUID().toString();
jdbcTemplate.update("insert into user_info(user_uuid, user_name)\n" +
"values (?,?);", userId, name);
// 使用一个RuntimeEx来触发事务回滚
int i = 1 / 0;
String orgUUID = UUID.randomUUID().toString();
jdbcTemplate.update("insert into org_info(org_uuid, org_name, user_uuid)\n" +
"VALUES (?, ?, ?);", orgUUID, orgName, userId);
}

}

应用事务

  • 编程式注解:使用 @Transactional 修饰的类或者方法,具有事务特性。(SpringBoot 时代一般用这个)
  • 声明式事务:通过定义配置文件,需要定义一套基于公司内部的方法命名规范。如以下的配置,update 以及 insert 为名的方法,使用事务,不然使用只读事务。
1
2
3
4
5
6
7
8
9
10
11
<tx:advice id="advice" transaction-manager="transactionManager">  
<tx:attributes>
<tx:method name="update*" propagation="REQUIRED" read-only="false" rollback-for="java.lang.Exception"/>
<tx:method name="insert" propagation="REQUIRED" read-only="false"/>
</tx:attributes>
</tx:advice>

<aop:config>
<aop:pointcut id="testService" expression="execution (* com.liweidan.service.MyBatisService.*(..))"/>
<aop:advisor advice-ref="advice" pointcut-ref="testService"/>
</aop:config>

在上面例子中的 UserServiceImpl 是使用编程式事务处理的,当没有加上 @Transactional 时,在 int i = 1 / 0; 处发生异常时,上面已经插入的用户信息并不会被回滚,这就不符合我们日常的业务需求了。OK,从这里开始将要开始看看 Spring 是如何应用事务到我们的代码上的。

依赖结构

OK,简单看看我们依赖了 spring-boot-starter-jdbc 都依赖了些什么东西。从图中可以看到,spring-jdbcspring-tx 以及 HicariCP 都进来了,spring-jdbc 模块也还好,封装了 JdbcTemplate 等一些偏向于原生 JDBC 的操作,spring-tx 就是此次需要说的重点了,事务管理模块,主要管理 Spring 自己定义的一些事务模型。HicariCP 则是一个高性能的数据库连接池。

基于AOP

我记得很清楚的,《从零开始架构WEB系统》中说到,可以使用 AOP 方式切入项目,获取 Connection 保存在线程的 ThreadLocal 中,在调用插入更新的时候,取出来开启事务,方法执行结束后判定是否是正常执行,如果遇到异常了,就 roolBack 插入的数据,否则 commit。那我们现在大概可以带着这个思路来看 Spring 源码。

自动配置

上一篇我们说过,SpringBoot 会自动扫描导入 org.springframework.boot.autoconfigure 包下的所有的配置,这次,事务的自动配置是 org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration。因为导入项目已经有 PlatformTransactionManager 类,所以该配置将被自动执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// TransactionAutoConfiguration.java: 
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(PlatformTransactionManager.class)
@AutoConfigureAfter({ JtaAutoConfiguration.class, HibernateJpaAutoConfiguration.class,
DataSourceTransactionManagerAutoConfiguration.class, Neo4jDataAutoConfiguration.class })
@EnableConfigurationProperties(TransactionProperties.class)
public class TransactionAutoConfiguration {

@Bean
@ConditionalOnMissingBean
public TransactionManagerCustomizers platformTransactionManagerCustomizers(
ObjectProvider<PlatformTransactionManagerCustomizer<?>> customizers) {
return new TransactionManagerCustomizers(customizers.orderedStream().collect(Collectors.toList()));
}

@Bean
@ConditionalOnMissingBean
@ConditionalOnSingleCandidate(ReactiveTransactionManager.class)
public TransactionalOperator transactionalOperator(ReactiveTransactionManager transactionManager) {
return TransactionalOperator.create(transactionManager);
}

@Configuration(proxyBeanMethods = false)
@ConditionalOnSingleCandidate(PlatformTransactionManager.class)
public static class TransactionTemplateConfiguration {

@Bean
@ConditionalOnMissingBean(TransactionOperations.class)
public TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) {
return new TransactionTemplate(transactionManager);
}

}

@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(TransactionManager.class)
@ConditionalOnMissingBean(AbstractTransactionManagementConfiguration.class)
public static class EnableTransactionManagementConfiguration {

@Configuration(proxyBeanMethods = false)
// 重点是这个注解,导入了些东西
@EnableTransactionManagement(proxyTargetClass = false)
@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false",
matchIfMissing = false)
public static class JdkDynamicAutoProxyConfiguration {

}

@Configuration(proxyBeanMethods = false)
@EnableTransactionManagement(proxyTargetClass = true)
@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
matchIfMissing = true)
public static class CglibAutoProxyConfiguration {

}

}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// EnableTransactionManagement.java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
// 导入一个Selector
@Import(TransactionManagementConfigurationSelector.class)
public @interface EnableTransactionManagement {

/**
* Indicate whether subclass-based (CGLIB) proxies are to be created ({@code true}) as
* opposed to standard Java interface-based proxies ({@code false}). The default is
* {@code false}. <strong>Applicable only if {@link #mode()} is set to
* {@link AdviceMode#PROXY}</strong>.
* <p>Note that setting this attribute to {@code true} will affect <em>all</em>
* Spring-managed beans requiring proxying, not just those marked with
* {@code @Transactional}. For example, other beans marked with Spring's
* {@code @Async} annotation will be upgraded to subclass proxying at the same
* time. This approach has no negative impact in practice unless one is explicitly
* expecting one type of proxy vs another, e.g. in tests.
*/
boolean proxyTargetClass() default false;

/**
* Indicate how transactional advice should be applied.
* <p><b>The default is {@link AdviceMode#PROXY}.</b>
* Please note that proxy mode allows for interception of calls through the proxy
* only. Local calls within the same class cannot get intercepted that way; an
* {@link Transactional} annotation on such a method within a local call will be
* ignored since Spring's interceptor does not even kick in for such a runtime
* scenario. For a more advanced mode of interception, consider switching this to
* {@link AdviceMode#ASPECTJ}.
*/
AdviceMode mode() default AdviceMode.PROXY;

/**
* Indicate the ordering of the execution of the transaction advisor
* when multiple advices are applied at a specific joinpoint.
* <p>The default is {@link Ordered#LOWEST_PRECEDENCE}.
*/
int order() default Ordered.LOWEST_PRECEDENCE;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// TransactionManagementConfigurationSelector.java
public class TransactionManagementConfigurationSelector extends AdviceModeImportSelector<EnableTransactionManagement> {

/**
* Returns {@link ProxyTransactionManagementConfiguration} or
* {@code AspectJ(Jta)TransactionManagementConfiguration} for {@code PROXY}
* and {@code ASPECTJ} values of {@link EnableTransactionManagement#mode()},
* respectively.
*/
@Override
protected String[] selectImports(AdviceMode adviceMode) {
switch (adviceMode) {
case PROXY:
return new String[] {AutoProxyRegistrar.class.getName(),
ProxyTransactionManagementConfiguration.class.getName()};
case ASPECTJ:
return new String[] {determineTransactionAspectClass()};
default:
return null;
}
}

private String determineTransactionAspectClass() {
return (ClassUtils.isPresent("javax.transaction.Transactional", getClass().getClassLoader()) ?
TransactionManagementConfigUtils.JTA_TRANSACTION_ASPECT_CONFIGURATION_CLASS_NAME :
TransactionManagementConfigUtils.TRANSACTION_ASPECT_CONFIGURATION_CLASS_NAME);
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
Collection<SourceClass> importCandidates, boolean checkForCircularImports) {

if (importCandidates.isEmpty()) {
return;
}

if (checkForCircularImports && isChainedImportOnStack(configClass)) {
this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
}
else {
this.importStack.push(configClass);
try {
for (SourceClass candidate : importCandidates) {
if (candidate.isAssignable(ImportSelector.class)) {
// Candidate class is an ImportSelector -> delegate to it to determine imports
Class<?> candidateClass = candidate.loadClass();
ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class,
this.environment, this.resourceLoader, this.registry);
if (selector instanceof DeferredImportSelector) {
this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
}
else {
// 在导入其他配置类的时候会使用上面的 selectImports 函数导入配置类
// 然后重新解析配置
String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames);
processImports(configClass, currentSourceClass, importSourceClasses, false);
}
// .....
}
}
}
}

来到这里,配置类的东西就已经被注册到 BeanFactory 中去,那么据之前的 SpringAOP 的介绍,BeanFactory 会在每次初始化 Bean 的时候,调用 BeanPostProcessor#postProcessAfterInitialization 这个函数来创建真实 Bean 的代理对象。 然后这件事情就交给了 AOP模块InfrastructureAdvisorAutoProxyCreator 来实现包装代理模式。

篇幅关系我只放关键的代码片段,快速过一下前面的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
throws BeanCreationException {
// ......
Object exposedObject = bean;
try {
populateBean(beanName, mbd, instanceWrapper);
// 包装实际Bean
exposedObject = initializeBean(beanName, exposedObject, mbd);
}
catch (Throwable ex) {
if (ex instanceof BeanCreationException && beanName.equals(((BeanCreationException) ex).getBeanName())) {
throw (BeanCreationException) ex;
}
else {
throw new BeanCreationException(
mbd.getResourceDescription(), beanName, "Initialization of bean failed", ex);
}
}
// ......
}
}
// initializeBean
protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) {
if (System.getSecurityManager() != null) {
AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
invokeAwareMethods(beanName, bean);
return null;
}, getAccessControlContext());
}
else {
invokeAwareMethods(beanName, bean);
}

Object wrappedBean = bean;
if (mbd == null !mbd.isSynthetic()) {
wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
}

try {
invokeInitMethods(beanName, wrappedBean, mbd);
}
catch (Throwable ex) {
throw new BeanCreationException(
(mbd != null ? mbd.getResourceDescription() : null),
beanName, "Invocation of init method failed", ex);
}
if (mbd == null !mbd.isSynthetic()) {
// 创建完成,使用集成的后处理器处理对应的Bean
wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
}

return wrappedBean;
}
// applyBeanPostProcessorsAfterInitialization使用后处理器包装Bean
public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName)
throws BeansException {

Object result = existingBean;
for (BeanPostProcessor processor : getBeanPostProcessors()) {
// 轮询到InfrastructureAdvisorAutoProxyCreator或AnnotationAwareAspectJAutoProxyCreator进行代理处理
Object current = processor.postProcessAfterInitialization(result, beanName);
if (current == null) {
return result;
}
result = current;
}
return result;
}

注:使用 InfrastructureAdvisorAutoProxyCreator 还是 AnnotationAwareAspectJAutoProxyCreator 取决于当前项目有没有依赖 Spring-AOP 模块,如果依赖 Spring-AOP 模块则会交给 AnnotationAwareAspectJAutoProxyCreator 进行处理。 两者的区别是:InfrastructureAdvisorAutoProxyCreator 不负责织入用户自定义的 AOP 类

然后,AbstractAdvisorAutoProxyCreator (两者的共同父类) 会找到所有的 Advisor 实现来织入这个实际对象的方法中。 因为在解析配置类的时候,BeanFactoryTransactionAttributeSourceAdvisor 这个类已经被注册到 BeanFactoryBeanDefinitionMap 中,所以第一个 Bean(大概率都是 Spring 自己需要的 Bean)创建的时候,就会触发上面自动配置中,创建 BeanFactoryTransactionAttributeSourceAdvisor 的配置类,调用方法进行相对应的创建。 然后又走了一遍 getBean 啊,doCreateBean 啊….创建 BeanFactoryTransactionAttributeSourceAdvisor。 好了,走完创建 BeanFactoryTransactionAttributeSourceAdvisor 的过程。我们现在需要看看怎么被织入。

织入方法

至于判定的方法,之前已经说过基于 @PointCut 切入,那么这个解析方式,应该很容易想到了吧,就是 类注解 + 方法级别注解 判定是否要切入当前方法。 上面两个织入类,无论怎么样都会来到这个函数织入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
return bean;
}
if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
return bean;
}
if (isInfrastructureClass(bean.getClass()) shouldSkip(bean.getClass(), beanName)) {
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}

// 获取当前Bean所需要的AOP拦截器链
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
if (specificInterceptors != DO_NOT_PROXY) {
this.advisedBeans.put(cacheKey, Boolean.TRUE);
// 进入创建代理对象
Object proxy = createProxy(
bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
this.proxyTypes.put(cacheKey, proxy.getClass());
return proxy;
}

this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}

进入配置并使用 ProxyFactory 来创建代理对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
@Nullable Object[] specificInterceptors, TargetSource targetSource) {

if (this.beanFactory instanceof ConfigurableListableBeanFactory) {
AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass);
}

ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.copyFrom(this);

if (!proxyFactory.isProxyTargetClass()) {
if (shouldProxyTargetClass(beanClass, beanName)) {
proxyFactory.setProxyTargetClass(true);
}
else {
evaluateProxyInterfaces(beanClass, proxyFactory);
}
}

Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
proxyFactory.addAdvisors(advisors);
proxyFactory.setTargetSource(targetSource);
customizeProxyFactory(proxyFactory);

proxyFactory.setFrozen(this.freezeProxy);
if (advisorsPreFiltered()) {
proxyFactory.setPreFiltered(true);
}

return proxyFactory.getProxy(getProxyClassLoader());
}

进入 getProxy 方法,通过配置来指定配置工厂(jdk代理cglib),然后调用两个工厂都有的 getProxy 来获取代理实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public Object getProxy(@Nullable ClassLoader classLoader) {
return createAopProxy().getProxy(classLoader);
}
protected final synchronized AopProxy createAopProxy() {
if (!this.active) {
activate();
}
return getAopProxyFactory().createAopProxy(this);
}
// DefaultAopProxyFactory.java
@Override
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
if (config.isOptimize() config.isProxyTargetClass() hasNoUserSuppliedProxyInterfaces(config)) {
Class<?> targetClass = config.getTargetClass();
if (targetClass == null) {
throw new AopConfigException("TargetSource cannot determine target class: " +
"Either an interface or a target is required for proxy creation.");
}
if (targetClass.isInterface() Proxy.isProxyClass(targetClass)) {
return new JdkDynamicAopProxy(config);
}
// 虽然我的类是带有接口的,但是来到这里,使用cglib进行创建
return new ObjenesisCglibAopProxy(config);
}
else {
return new JdkDynamicAopProxy(config);
}
}

SpringBoot代理模式

上面跑出来的小问题,为啥带有接口还是使用了 cglib 创建,跟我之前读 Spring-AOP 的时候是不一样的结果。 这里就要说到 SpringBoot 的自动创建配置了:

1
2
3
4
5
6
7
// META-INF/additional-spring-configuration-metadata.json
{
"name": "spring.aop.proxy-target-class",
"type": "java.lang.Boolean",
"description": "Whether subclass-based (CGLIB) proxies are to be created (true), as opposed to standard Java interface-based proxies (false).",
"defaultValue": true
},

然而搜遍谷歌 Spring 项目,只看到 Spring 成员说了一句:

意思是 Cglib 代理能够减少类转换异常。

织入拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Override
public Object getProxy(@Nullable ClassLoader classLoader) {
// 省略部分代码
// Configure CGLIB Enhancer...
Enhancer enhancer = createEnhancer();
if (classLoader != null) {
enhancer.setClassLoader(classLoader);
if (classLoader instanceof SmartClassLoader &&
((SmartClassLoader) classLoader).isClassReloadable(proxySuperClass)) {
enhancer.setUseCache(false);
}
}
enhancer.setSuperclass(proxySuperClass);
enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised));
enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
enhancer.setStrategy(new ClassLoaderAwareGeneratorStrategy(classLoader));

// 在Callback这里即会织入BeanFactoryTransactionAttributeSourceAdvisor中的拦截器:
// TransactionInterceptor
Callback[] callbacks = getCallbacks(rootClass);
Class<?>[] types = new Class<?>[callbacks.length];
for (int x = 0; x < types.length; x++) {
types[x] = callbacks[x].getClass();
}
// fixedInterceptorMap only populated at this point, after getCallbacks call above
enhancer.setCallbackFilter(new ProxyCallbackFilter(
this.advised.getConfigurationOnlyCopy(), this.fixedInterceptorMap, this.fixedInterceptorOffset));
enhancer.setCallbackTypes(types);

// Generate the proxy class and create a proxy instance.
return createProxyClassAndInstance(enhancer, callbacks);
}
catch (CodeGenerationException IllegalArgumentException ex) {
throw new AopConfigException("Could not generate CGLIB subclass of " + this.advised.getTargetClass() +
": Common causes of this problem include using a final class or a non-visible class",
ex);
}
catch (Throwable ex) {
// TargetSource.getTarget() failed
throw new AopConfigException("Unexpected AOP exception", ex);
}
}

拦截器织入完成接下来就需要看看怎么被调用的了。


插入事务

真男人要直接,直接在 Controller 打断点,进入业务方法之前,会先进入 TransactionInterceptor#invoke

1
2
3
4
5
6
7
8
9
@Override
@Nullable
public Object invoke(MethodInvocation invocation) throws Throwable {
// 获取目标类
Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);

// 开启事务执行方法
return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
}

呃,代码还是挺长的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
@Nullable
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
final InvocationCallback invocation) throws Throwable {

// 获取所有配置的事务属性元数据类,如果为null,代表默认事务(默认是autoCommit=true)
TransactionAttributeSource tas = getTransactionAttributeSource();
// 从缓存中命中当前方法的事务配置
final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
// 获取事务管理器
final TransactionManager tm = determineTransactionManager(txAttr);

// Reactive环境走这里,先跳过
if (this.reactiveAdapterRegistry != null && tm instanceof ReactiveTransactionManager) {
ReactiveTransactionSupport txSupport = this.transactionSupportCache.computeIfAbsent(method, key -> {
if (KotlinDetector.isKotlinType(method.getDeclaringClass()) && KotlinDelegate.isSuspend(method)) {
throw new TransactionUsageException(
"Unsupported annotated transaction on suspending function detected: " + method +
". Use TransactionalOperator.transactional extensions instead.");
}
ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(method.getReturnType());
if (adapter == null) {
throw new IllegalStateException("Cannot apply reactive transaction to non-reactive return type: " +
method.getReturnType());
}
return new ReactiveTransactionSupport(adapter);
});
return txSupport.invokeWithinTransaction(
method, targetClass, invocation, txAttr, (ReactiveTransactionManager) tm);
}

// 获取Spring提供的统一事务管理器
PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);

if (txAttr == null !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
// 创建或者加入当前事务
TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);

Object retVal;
try {
// 执行目标方法,如果有下一个AOP链则接下去执行
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// 遇到异常,回滚数据库事务
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
cleanupTransactionInfo(txInfo);
}

// 响应式数据库操作 也是先跳过
if (vavrPresent && VavrDelegate.isVavrTry(retVal)) {
// Set rollback-only in case of Vavr failure matching our rollback rules...
TransactionStatus status = txInfo.getTransactionStatus();
if (status != null && txAttr != null) {
retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
}
}

// 提交数据库事务
commitTransactionAfterReturning(txInfo);
return retVal;
}
// CallbackPreferringPlatformTransactionManager 可回调的管理器
// 也是响应式的内容
else {
final ThrowableHolder throwableHolder = new ThrowableHolder();

// It's a CallbackPreferringPlatformTransactionManager: pass a TransactionCallback in.
try {
Object result = ((CallbackPreferringPlatformTransactionManager) ptm).execute(txAttr, status -> {
TransactionInfo txInfo = prepareTransactionInfo(ptm, txAttr, joinpointIdentification, status);
try {
Object retVal = invocation.proceedWithInvocation();
if (vavrPresent && VavrDelegate.isVavrTry(retVal)) {
// Set rollback-only in case of Vavr failure matching our rollback rules...
retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
}
return retVal;
}
catch (Throwable ex) {
if (txAttr.rollbackOn(ex)) {
// A RuntimeException: will lead to a rollback.
if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}
else {
throw new ThrowableHolderException(ex);
}
}
else {
// A normal return value: will lead to a commit.
throwableHolder.throwable = ex;
return null;
}
}
finally {
cleanupTransactionInfo(txInfo);
}
});

// Check result state: It might indicate a Throwable to rethrow.
if (throwableHolder.throwable != null) {
throw throwableHolder.throwable;
}
return result;
}
catch (ThrowableHolderException ex) {
throw ex.getCause();
}
catch (TransactionSystemException ex2) {
if (throwableHolder.throwable != null) {
logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
ex2.initApplicationException(throwableHolder.throwable);
}
throw ex2;
}
catch (Throwable ex2) {
if (throwableHolder.throwable != null) {
logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
}
throw ex2;
}
}
}

那下面就一步一步来看。

TransactionAttribute事务属性

这是一个定义了事务级别以及其他比如超时信息的事务元信息类,当前获取的是一个 RuleBasedTransactionAttribute 基于一定规则的事务属性,定义了必须在抛出 Runtime 异常的时候回滚数据库。 父级是 TransactionDefinition,这可是一个元老级别的类了,定义了事务管理器常见所需的隔离级别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
public interface TransactionDefinition {

// ------------------------------ Spring事务隔离级别 ------------------------------
// 如果当前没有事务,则创建一个新事务
int PROPAGATION_REQUIRED = 0;

// 如果当前没有事务则以无事务的方式运行
int PROPAGATION_SUPPORTS = 1;

// 当前有事务就加入没有就抛异常
int PROPAGATION_MANDATORY = 2;

// 阻塞当前事务,创建一个子事务
int PROPAGATION_REQUIRES_NEW = 3;

// 一直以无事务的状态运行
int PROPAGATION_NOT_SUPPORTED = 4;

// 如果当前有事务则抛出异常
int PROPAGATION_NEVER = 5;

// 如果存在事务则在嵌套事务中运行
// 行为类似于 PROPAGATION_REQUIRED
int PROPAGATION_NESTED = 6;

// ------------------------------ jdbc原生事务隔离级别 ------------------------------

// 使用数据库默认的级别
int ISOLATION_DEFAULT = -1;

// 未提交读
int ISOLATION_READ_UNCOMMITTED = 1; // same as java.sql.Connection.TRANSACTION_READ_UNCOMMITTED;

// 不可重复读
int ISOLATION_READ_COMMITTED = 2; // same as java.sql.Connection.TRANSACTION_READ_COMMITTED;


// 可重复读
int ISOLATION_REPEATABLE_READ = 4; // same as java.sql.Connection.TRANSACTION_REPEATABLE_READ;

// 串行执行事务
int ISOLATION_SERIALIZABLE = 8; // same as java.sql.Connection.TRANSACTION_SERIALIZABLE;


/**
* Use the default timeout of the underlying transaction system,
* or none if timeouts are not supported.
*/
int TIMEOUT_DEFAULT = -1;


/**
* Return the propagation behavior.
* <p>Must return one of the {@code PROPAGATION_XXX} constants
* defined on {@link TransactionDefinition this interface}.
* <p>The default is {@link #PROPAGATION_REQUIRED}.
* @return the propagation behavior
* @see #PROPAGATION_REQUIRED
* @see org.springframework.transaction.support.TransactionSynchronizationManager#isActualTransactionActive()
*/
default int getPropagationBehavior() {
return PROPAGATION_REQUIRED;
}

/**
* Return the isolation level.
* <p>Must return one of the {@code ISOLATION_XXX} constants defined on
* {@link TransactionDefinition this interface}. Those constants are designed
* to match the values of the same constants on {@link java.sql.Connection}.
* <p>Exclusively designed for use with {@link #PROPAGATION_REQUIRED} or
* {@link #PROPAGATION_REQUIRES_NEW} since it only applies to newly started
* transactions. Consider switching the "validateExistingTransactions" flag to
* "true" on your transaction manager if you'd like isolation level declarations
* to get rejected when participating in an existing transaction with a different
* isolation level.
* <p>The default is {@link #ISOLATION_DEFAULT}. Note that a transaction manager
* that does not support custom isolation levels will throw an exception when
* given any other level than {@link #ISOLATION_DEFAULT}.
* @return the isolation level
* @see #ISOLATION_DEFAULT
* @see org.springframework.transaction.support.AbstractPlatformTransactionManager#setValidateExistingTransaction
*/
default int getIsolationLevel() {
return ISOLATION_DEFAULT;
}

/**
* Return the transaction timeout.
* <p>Must return a number of seconds, or {@link #TIMEOUT_DEFAULT}.
* <p>Exclusively designed for use with {@link #PROPAGATION_REQUIRED} or
* {@link #PROPAGATION_REQUIRES_NEW} since it only applies to newly started
* transactions.
* <p>Note that a transaction manager that does not support timeouts will throw
* an exception when given any other timeout than {@link #TIMEOUT_DEFAULT}.
* <p>The default is {@link #TIMEOUT_DEFAULT}.
* @return the transaction timeout
*/
default int getTimeout() {
return TIMEOUT_DEFAULT;
}

/**
* Return whether to optimize as a read-only transaction.
* <p>The read-only flag applies to any transaction context, whether backed
* by an actual resource transaction ({@link #PROPAGATION_REQUIRED}/
* {@link #PROPAGATION_REQUIRES_NEW}) or operating non-transactionally at
* the resource level ({@link #PROPAGATION_SUPPORTS}). In the latter case,
* the flag will only apply to managed resources within the application,
* such as a Hibernate {@code Session}.
* <p>This just serves as a hint for the actual transaction subsystem;
* it will <i>not necessarily</i> cause failure of write access attempts.
* A transaction manager which cannot interpret the read-only hint will
* <i>not</i> throw an exception when asked for a read-only transaction.
* @return {@code true} if the transaction is to be optimized as read-only
* ({@code false} by default)
* @see org.springframework.transaction.support.TransactionSynchronization#beforeCommit(boolean)
* @see org.springframework.transaction.support.TransactionSynchronizationManager#isCurrentTransactionReadOnly()
*/
default boolean isReadOnly() {
return false;
}

/**
* Return the name of this transaction. Can be {@code null}.
* <p>This will be used as the transaction name to be shown in a
* transaction monitor, if applicable (for example, WebLogic's).
* <p>In case of Spring's declarative transactions, the exposed name will be
* the {@code fully-qualified class name + "." + method name} (by default).
* @return the name of this transaction ({@code null} by default}
* @see org.springframework.transaction.interceptor.TransactionAspectSupport
* @see org.springframework.transaction.support.TransactionSynchronizationManager#getCurrentTransactionName()
*/
@Nullable
default String getName() {
return null;
}


// Static builder methods

/**
* Return an unmodifiable {@code TransactionDefinition} with defaults.
* <p>For customization purposes, use the modifiable
* {@link org.springframework.transaction.support.DefaultTransactionDefinition}
* instead.
* @since 5.2
*/
static TransactionDefinition withDefaults() {
return StaticTransactionDefinition.INSTANCE;
}

}

那这个东西用在哪里呢,后面就会看到处理方式了,先放一放。

事务管理器

这一步没做什么,就当是单纯获得了一个事务管理器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Nullable
protected TransactionManager determineTransactionManager(@Nullable TransactionAttribute txAttr) {
// Do not attempt to lookup tx manager if no tx attributes are set
if (txAttr == null this.beanFactory == null) {
return getTransactionManager();
}

String qualifier = txAttr.getQualifier();
if (StringUtils.hasText(qualifier)) {
return determineQualifiedTransactionManager(this.beanFactory, qualifier);
}
else if (StringUtils.hasText(this.transactionManagerBeanName)) {
return determineQualifiedTransactionManager(this.beanFactory, this.transactionManagerBeanName);
}
else {
TransactionManager defaultTransactionManager = getTransactionManager();
if (defaultTransactionManager == null) {
defaultTransactionManager = this.transactionManagerCache.get(DEFAULT_TRANSACTION_MANAGER_KEY);
if (defaultTransactionManager == null) {
defaultTransactionManager = this.beanFactory.getBean(TransactionManager.class);
this.transactionManagerCache.putIfAbsent(
DEFAULT_TRANSACTION_MANAGER_KEY, defaultTransactionManager);
}
}
return defaultTransactionManager;
}
}

开启数据库事务

可以看到,传递了一个事务定义元数据来创建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
protected TransactionInfo createTransactionIfNecessary(@Nullable PlatformTransactionManager tm,
@Nullable TransactionAttribute txAttr, final String joinpointIdentification) {

// 处理名字
if (txAttr != null && txAttr.getName() == null) {
txAttr = new DelegatingTransactionAttribute(txAttr) {
@Override
public String getName() {
return joinpointIdentification;
}
};
}

TransactionStatus status = null;
if (txAttr != null) {
if (tm != null) {
status = tm.getTransaction(txAttr);
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Skipping transactional joinpoint [" + joinpointIdentification +
"] because no transaction manager has been configured");
}
}
}
// 管理当前开启的事务管理器
return prepareTransactionInfo(tm, txAttr, joinpointIdentification, status);
}

首先,先根据当前的事务管理级别创建 TransactionStatus

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
@Override
public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
throws TransactionException {

// 获取 TransactionDefinition,如果配置中没有定义则获取默认的事务定义.
TransactionDefinition def = (definition != null ? definition : TransactionDefinition.withDefaults());

// 获取事务状态对象
Object transaction = doGetTransaction();
boolean debugEnabled = logger.isDebugEnabled();

if (isExistingTransaction(transaction)) {
// 当前存在事务,根据不同等级返回 TransactionStatus.
return handleExistingTransaction(def, transaction, debugEnabled);
}

// 超时的话抛出异常.
if (def.getTimeout() < TransactionDefinition.TIMEOUT_DEFAULT) {
throw new InvalidTimeoutException("Invalid transaction timeout", def.getTimeout());
}

// 不存在事务的时候,需要根据上面的事务定义来继续行为.
if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) {
// 这个等级是没有事务即抛出异常
throw new IllegalTransactionStateException(
"No existing transaction found for transaction marked with propagation 'mandatory'");
}
else if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED
def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW
def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
SuspendedResourcesHolder suspendedResources = suspend(null);
if (debugEnabled) {
logger.debug("Creating new transaction with name [" + def.getName() + "]: " + def);
}
try {
// 创建新的 TransactionStatus 然后开启数据库事务
boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
// 创建一个新的DefaultTransactionStatus贯穿整个事务
DefaultTransactionStatus status = newTransactionStatus(
def, transaction, true, newSynchronization, debugEnabled, suspendedResources);
// 开启新事务
doBegin(transaction, def);
// 设置激活程序中的事务管理器,设置只读属性、事务名字、隔离级别等信息
prepareSynchronization(status, def);
return status;
}
catch (RuntimeException Error ex) {
resume(null, suspendedResources);
throw ex;
}
}
else {
// Create "empty" transaction: no actual transaction, but potentially synchronization.
if (def.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT && logger.isWarnEnabled()) {
logger.warn("Custom isolation level specified but no actual transaction initiated; " +
"isolation level will effectively be ignored: " + def);
}
boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
return prepareTransactionStatus(def, null, true, newSynchronization, debugEnabled, null);
}
}

OK,接下来看看 doBegin 怎么管理 Connection

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// 开启事务管理器
@Override
protected void doBegin(Object transaction, TransactionDefinition definition) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
Connection con = null;

try {
if (!txObject.hasConnectionHolder()
txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
// 通过数据库连接池拿到 Connection
Connection newCon = obtainDataSource().getConnection();
if (logger.isDebugEnabled()) {
logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
}
// 使用ConnectionHolder管理当前的连接
txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
}

txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
con = txObject.getConnectionHolder().getConnection();

// Connection属性设置(之前的隔离级别、是否只读)
Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
txObject.setPreviousIsolationLevel(previousIsolationLevel);
txObject.setReadOnly(definition.isReadOnly());

// 修改Connection的只读状态
if (con.getAutoCommit()) {
txObject.setMustRestoreAutoCommit(true);
if (logger.isDebugEnabled()) {
logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
}
con.setAutoCommit(false);
}

// 如果当前是只读事务则发送 SET TRANSACTION READ ONLY 给数据库切换事务状态
prepareTransactionalConnection(con, definition);
txObject.getConnectionHolder().setTransactionActive(true);

int timeout = determineTimeout(definition);
if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
}

// 把ConnectionHolder绑定到TransactionSynchronizationManager
if (txObject.isNewConnectionHolder()) {
TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());
}
}

catch (Throwable ex) {
if (txObject.isNewConnectionHolder()) {
DataSourceUtils.releaseConnection(con, obtainDataSource());
txObject.setConnectionHolder(null, false);
}
throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex);
}
}

开始执行

为了防止需要拖拉到上面去阅读,我先把之前执行到哪个地方放在这里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
@Nullable
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
final InvocationCallback invocation) throws Throwable {

// 获取所有配置的事务属性元数据类,如果为null,代表默认事务(默认是autoCommit=true)
TransactionAttributeSource tas = getTransactionAttributeSource();
// 从缓存中命中当前方法的事务配置
final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
// 获取事务管理器
final TransactionManager tm = determineTransactionManager(txAttr);

// Reactive环境走这里,先跳过
if (this.reactiveAdapterRegistry != null && tm instanceof ReactiveTransactionManager) {
ReactiveTransactionSupport txSupport = this.transactionSupportCache.computeIfAbsent(method, key -> {
if (KotlinDetector.isKotlinType(method.getDeclaringClass()) && KotlinDelegate.isSuspend(method)) {
throw new TransactionUsageException(
"Unsupported annotated transaction on suspending function detected: " + method +
". Use TransactionalOperator.transactional extensions instead.");
}
ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(method.getReturnType());
if (adapter == null) {
throw new IllegalStateException("Cannot apply reactive transaction to non-reactive return type: " +
method.getReturnType());
}
return new ReactiveTransactionSupport(adapter);
});
return txSupport.invokeWithinTransaction(
method, targetClass, invocation, txAttr, (ReactiveTransactionManager) tm);
}

// 获取Spring提供的统一事务管理器
PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);

if (txAttr == null !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
// 创建或者加入当前事务
TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);

Object retVal;
try {
// 执行目标方法,如果有下一个AOP链则接下去执行
// ---------------> 已经准备好了事务信息,可以开始执行整个AOP链条了
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// 遇到异常,回滚数据库事务
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
// 清空当前事务,如果当前是嵌套事务,管理器会重新拿到之前的事务
cleanupTransactionInfo(txInfo);
}

// 响应式数据库操作 也是先跳过
if (vavrPresent && VavrDelegate.isVavrTry(retVal)) {
// Set rollback-only in case of Vavr failure matching our rollback rules...
TransactionStatus status = txInfo.getTransactionStatus();
if (status != null && txAttr != null) {
retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
}
}

// 提交数据库事务
commitTransactionAfterReturning(txInfo);
return retVal;
}
// CallbackPreferringPlatformTransactionManager 可回调的管理器
// 也是响应式的内容
else {
// 省略响应式内容.........
}
}

正常提交

拿到事务管理器,提交事务:

1
2
3
4
5
6
7
8
protected void commitTransactionAfterReturning(@Nullable TransactionInfo txInfo) {
if (txInfo != null && txInfo.getTransactionStatus() != null) {
if (logger.isTraceEnabled()) {
logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() + "]");
}
txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
}
}

需要经过判断来做是否真正提交:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Override
public final void commit(TransactionStatus status) throws TransactionException {
if (status.isCompleted()) {
throw new IllegalTransactionStateException(
"Transaction is already completed - do not call commit or rollback more than once per transaction");
}

DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;

// 根据用户TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();设置
// 是否不让提交(比如校验没有通过,但是没有抛出异常) 如果设置了则回滚数据库
if (defStatus.isLocalRollbackOnly()) {
if (defStatus.isDebug()) {
logger.debug("Transactional code has requested rollback");
}
processRollback(defStatus, false);
return;
}

// 这里是判断全局事务中配置了只能在全局回滚并且当前已经出现错误,如果当前是内部事务,会标记业务层事务出现错误
if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) {
if (defStatus.isDebug()) {
logger.debug("Global transaction is marked as rollback-only but transactional code requested commit");
}
processRollback(defStatus, true);
return;
}

// 处理提交事务
processCommit(defStatus);
}

处理提交,可以说,我们项目中使用到提交事务生命周期都在这里被调用到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
private void processCommit(DefaultTransactionStatus status) throws TransactionException {
try {
boolean beforeCompletionInvoked = false;

try {
boolean unexpectedRollback = false;
// 下面三个是调用相对应的生命周期
prepareForCommit(status);
triggerBeforeCommit(status);
triggerBeforeCompletion(status);
beforeCompletionInvoked = true;

// SavePoint 先不看
if (status.hasSavepoint()) {
if (status.isDebug()) {
logger.debug("Releasing transaction savepoint");
}
unexpectedRollback = status.isGlobalRollbackOnly();
status.releaseHeldSavepoint();
}
// 当前是一个新的事务
else if (status.isNewTransaction()) {
if (status.isDebug()) {
logger.debug("Initiating transaction commit");
}
unexpectedRollback = status.isGlobalRollbackOnly();
// 真正做提交的地方
doCommit(status);
}
else if (isFailEarlyOnGlobalRollbackOnly()) {
unexpectedRollback = status.isGlobalRollbackOnly();
}

// Throw UnexpectedRollbackException if we have a global rollback-only
// marker but still didn't get a corresponding exception from commit.
if (unexpectedRollback) {
throw new UnexpectedRollbackException(
"Transaction silently rolled back because it has been marked as rollback-only");
}
}
catch (UnexpectedRollbackException ex) {
// can only be caused by doCommit
triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK);
throw ex;
}
catch (TransactionException ex) {
// can only be caused by doCommit
if (isRollbackOnCommitFailure()) {
doRollbackOnCommitException(status, ex);
}
else {
triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN);
}
throw ex;
}
catch (RuntimeException Error ex) {
if (!beforeCompletionInvoked) {
triggerBeforeCompletion(status);
}
doRollbackOnCommitException(status, ex);
throw ex;
}

// Trigger afterCommit callbacks, with an exception thrown there
// propagated to callers but the transaction still considered as committed.
try {
// 调用事务生命周期的后处理器
triggerAfterCommit(status);
}
finally {
triggerAfterCompletion(status, TransactionSynchronization.STATUS_COMMITTED);
}

}
finally {
cleanupAfterCompletion(status);
}
}

拿到 Connection 调用 Commit 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
protected void doCommit(DefaultTransactionStatus status) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
Connection con = txObject.getConnectionHolder().getConnection();
if (status.isDebug()) {
logger.debug("Committing JDBC transaction on Connection [" + con + "]");
}
try {
con.commit();
}
catch (SQLException ex) {
throw new TransactionSystemException("Could not commit JDBC transaction", ex);
}
}

回滚数据库

业务代码加一个除以 0 的计算:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Service
public class UserServiceImpl implements UserService {

@Autowired
private JdbcTemplate jdbcTemplate;

@Override
@Transactional
public void add(String name, String orgName) {
String userId = UUID.randomUUID().toString();
jdbcTemplate.update("insert into user_info(user_uuid, user_name)\n" +
"values (?,?);", userId, name);
int i = 1 / 0;
String orgUUID = UUID.randomUUID().toString();
jdbcTemplate.update("insert into org_info(org_uuid, org_name, user_uuid)\n" +
"VALUES (?, ?, ?);", orgUUID, orgName, userId);
}

}

那么出现了异常了,这一步会来到 try-catch 中的 catch:

1
2
3
4
5
6
7
8
9
10
11
12
try {
// 执行目标方法,如果有下一个AOP链则接下去执行
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// 遇到异常,回滚数据库事务
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
cleanupTransactionInfo(txInfo);
}

这个是简单了很多了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
if (txInfo != null && txInfo.getTransactionStatus() != null) {
if (logger.isTraceEnabled()) {
logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() +
"] after exception: " + ex);
}
// 判断当前抛出的异常是否需要回滚数据库
if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
try {
txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
}
catch (TransactionSystemException ex2) {
logger.error("Application exception overridden by rollback exception", ex);
ex2.initApplicationException(ex);
throw ex2;
}
catch (RuntimeException Error ex2) {
logger.error("Application exception overridden by rollback exception", ex);
throw ex2;
}
}
// 如果不会滚,继续提交
else {
// We don't roll back on this exception.
// Will still roll back if TransactionStatus.isRollbackOnly() is true.
try {
txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
}
catch (TransactionSystemException ex2) {
logger.error("Application exception overridden by commit exception", ex);
ex2.initApplicationException(ex);
throw ex2;
}
catch (RuntimeException Error ex2) {
logger.error("Application exception overridden by commit exception", ex);
throw ex2;
}
}
}
}

好吧来到了处理方法:

1
2
3
4
5
6
7
8
9
10
@Override
public final void rollback(TransactionStatus status) throws TransactionException {
if (status.isCompleted()) {
throw new IllegalTransactionStateException(
"Transaction is already completed - do not call commit or rollback more than once per transaction");
}

DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;
processRollback(defStatus, false);
}

那么其实跟提交的套路是一样的,触发一系列的生命周期:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
private void processRollback(DefaultTransactionStatus status, boolean unexpected) {
try {
boolean unexpectedRollback = unexpected;

try {
triggerBeforeCompletion(status);

if (status.hasSavepoint()) {
if (status.isDebug()) {
logger.debug("Rolling back transaction to savepoint");
}
status.rollbackToHeldSavepoint();
}
else if (status.isNewTransaction()) {
if (status.isDebug()) {
logger.debug("Initiating transaction rollback");
}
doRollback(status);
}
else {
// Participating in larger transaction
if (status.hasTransaction()) {
if (status.isLocalRollbackOnly() isGlobalRollbackOnParticipationFailure()) {
if (status.isDebug()) {
logger.debug("Participating transaction failed - marking existing transaction as rollback-only");
}
doSetRollbackOnly(status);
}
else {
if (status.isDebug()) {
logger.debug("Participating transaction failed - letting transaction originator decide on rollback");
}
}
}
else {
logger.debug("Should roll back transaction but cannot - no transaction available");
}
// Unexpected rollback only matters here if we're asked to fail early
if (!isFailEarlyOnGlobalRollbackOnly()) {
unexpectedRollback = false;
}
}
}
catch (RuntimeException Error ex) {
triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN);
throw ex;
}

triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK);

// Raise UnexpectedRollbackException if we had a global rollback-only marker
if (unexpectedRollback) {
throw new UnexpectedRollbackException(
"Transaction rolled back because it has been marked as rollback-only");
}
}
finally {
cleanupAfterCompletion(status);
}
}

拿到 Connection 进行回滚。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
protected void doRollback(DefaultTransactionStatus status) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
Connection con = txObject.getConnectionHolder().getConnection();
if (status.isDebug()) {
logger.debug("Rolling back JDBC transaction on Connection [" + con + "]");
}
try {
con.rollback();
}
catch (SQLException ex) {
throw new TransactionSystemException("Could not roll back JDBC transaction", ex);
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
public interface TransactionDefinition {

// ------------------------------ Spring事务隔离级别 ------------------------------
// 如果当前没有事务,则创建一个新事务
int PROPAGATION_REQUIRED = 0;

// 如果当前没有事务则以无事务的方式运行
int PROPAGATION_SUPPORTS = 1;

// 当前有事务就加入没有就抛异常
int PROPAGATION_MANDATORY = 2;

// 阻塞当前事务,创建一个子事务
int PROPAGATION_REQUIRES_NEW = 3;

// 一直以无事务的状态运行
int PROPAGATION_NOT_SUPPORTED = 4;

// 如果当前有事务则抛出异常
int PROPAGATION_NEVER = 5;

// 如果存在事务则在嵌套事务中运行
// 行为类似于 PROPAGATION_REQUIRED
int PROPAGATION_NESTED = 6;

// ------------------------------ jdbc原生事务隔离级别 ------------------------------

// 使用数据库默认的级别
int ISOLATION_DEFAULT = -1;

// 未提交读
int ISOLATION_READ_UNCOMMITTED = 1; // same as java.sql.Connection.TRANSACTION_READ_UNCOMMITTED;

// 不可重复读
int ISOLATION_READ_COMMITTED = 2; // same as java.sql.Connection.TRANSACTION_READ_COMMITTED;


// 可重复读
int ISOLATION_REPEATABLE_READ = 4; // same as java.sql.Connection.TRANSACTION_REPEATABLE_READ;

// 串行执行事务
int ISOLATION_SERIALIZABLE = 8; // same as java.sql.Connection.TRANSACTION_SERIALIZABLE;


/**
* Use the default timeout of the underlying transaction system,
* or none if timeouts are not supported.
*/
int TIMEOUT_DEFAULT = -1;


/**
* Return the propagation behavior.
* <p>Must return one of the {@code PROPAGATION_XXX} constants
* defined on {@link TransactionDefinition this interface}.
* <p>The default is {@link #PROPAGATION_REQUIRED}.
* @return the propagation behavior
* @see #PROPAGATION_REQUIRED
* @see org.springframework.transaction.support.TransactionSynchronizationManager#isActualTransactionActive()
*/
default int getPropagationBehavior() {
return PROPAGATION_REQUIRED;
}

/**
* Return the isolation level.
* <p>Must return one of the {@code ISOLATION_XXX} constants defined on
* {@link TransactionDefinition this interface}. Those constants are designed
* to match the values of the same constants on {@link java.sql.Connection}.
* <p>Exclusively designed for use with {@link #PROPAGATION_REQUIRED} or
* {@link #PROPAGATION_REQUIRES_NEW} since it only applies to newly started
* transactions. Consider switching the "validateExistingTransactions" flag to
* "true" on your transaction manager if you'd like isolation level declarations
* to get rejected when participating in an existing transaction with a different
* isolation level.
* <p>The default is {@link #ISOLATION_DEFAULT}. Note that a transaction manager
* that does not support custom isolation levels will throw an exception when
* given any other level than {@link #ISOLATION_DEFAULT}.
* @return the isolation level
* @see #ISOLATION_DEFAULT
* @see org.springframework.transaction.support.AbstractPlatformTransactionManager#setValidateExistingTransaction
*/
default int getIsolationLevel() {
return ISOLATION_DEFAULT;
}

/**
* Return the transaction timeout.
* <p>Must return a number of seconds, or {@link #TIMEOUT_DEFAULT}.
* <p>Exclusively designed for use with {@link #PROPAGATION_REQUIRED} or
* {@link #PROPAGATION_REQUIRES_NEW} since it only applies to newly started
* transactions.
* <p>Note that a transaction manager that does not support timeouts will throw
* an exception when given any other timeout than {@link #TIMEOUT_DEFAULT}.
* <p>The default is {@link #TIMEOUT_DEFAULT}.
* @return the transaction timeout
*/
default int getTimeout() {
return TIMEOUT_DEFAULT;
}

/**
* Return whether to optimize as a read-only transaction.
* <p>The read-only flag applies to any transaction context, whether backed
* by an actual resource transaction ({@link #PROPAGATION_REQUIRED}/
* {@link #PROPAGATION_REQUIRES_NEW}) or operating non-transactionally at
* the resource level ({@link #PROPAGATION_SUPPORTS}). In the latter case,
* the flag will only apply to managed resources within the application,
* such as a Hibernate {@code Session}.
* <p>This just serves as a hint for the actual transaction subsystem;
* it will <i>not necessarily</i> cause failure of write access attempts.
* A transaction manager which cannot interpret the read-only hint will
* <i>not</i> throw an exception when asked for a read-only transaction.
* @return {@code true} if the transaction is to be optimized as read-only
* ({@code false} by default)
* @see org.springframework.transaction.support.TransactionSynchronization#beforeCommit(boolean)
* @see org.springframework.transaction.support.TransactionSynchronizationManager#isCurrentTransactionReadOnly()
*/
default boolean isReadOnly() {
return false;
}

/**
* Return the name of this transaction. Can be {@code null}.
* <p>This will be used as the transaction name to be shown in a
* transaction monitor, if applicable (for example, WebLogic's).
* <p>In case of Spring's declarative transactions, the exposed name will be
* the {@code fully-qualified class name + "." + method name} (by default).
* @return the name of this transaction ({@code null} by default}
* @see org.springframework.transaction.interceptor.TransactionAspectSupport
* @see org.springframework.transaction.support.TransactionSynchronizationManager#getCurrentTransactionName()
*/
@Nullable
default String getName() {
return null;
}


// Static builder methods

/**
* Return an unmodifiable {@code TransactionDefinition} with defaults.
* <p>For customization purposes, use the modifiable
* {@link org.springframework.transaction.support.DefaultTransactionDefinition}
* instead.
* @since 5.2
*/
static TransactionDefinition withDefaults() {
return StaticTransactionDefinition.INSTANCE;
}

}

那这个东西用在哪里呢,后面就会看到处理方式了,先放一放。

事务管理器

这一步没做什么,就当是单纯获得了一个事务管理器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Nullable
protected TransactionManager determineTransactionManager(@Nullable TransactionAttribute txAttr) {
// Do not attempt to lookup tx manager if no tx attributes are set
if (txAttr == null this.beanFactory == null) {
return getTransactionManager();
}

String qualifier = txAttr.getQualifier();
if (StringUtils.hasText(qualifier)) {
return determineQualifiedTransactionManager(this.beanFactory, qualifier);
}
else if (StringUtils.hasText(this.transactionManagerBeanName)) {
return determineQualifiedTransactionManager(this.beanFactory, this.transactionManagerBeanName);
}
else {
TransactionManager defaultTransactionManager = getTransactionManager();
if (defaultTransactionManager == null) {
defaultTransactionManager = this.transactionManagerCache.get(DEFAULT_TRANSACTION_MANAGER_KEY);
if (defaultTransactionManager == null) {
defaultTransactionManager = this.beanFactory.getBean(TransactionManager.class);
this.transactionManagerCache.putIfAbsent(
DEFAULT_TRANSACTION_MANAGER_KEY, defaultTransactionManager);
}
}
return defaultTransactionManager;
}
}

开启数据库事务

可以看到,传递了一个事务定义元数据来创建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
protected TransactionInfo createTransactionIfNecessary(@Nullable PlatformTransactionManager tm,
@Nullable TransactionAttribute txAttr, final String joinpointIdentification) {

// 处理名字
if (txAttr != null && txAttr.getName() == null) {
txAttr = new DelegatingTransactionAttribute(txAttr) {
@Override
public String getName() {
return joinpointIdentification;
}
};
}

TransactionStatus status = null;
if (txAttr != null) {
if (tm != null) {
status = tm.getTransaction(txAttr);
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Skipping transactional joinpoint [" + joinpointIdentification +
"] because no transaction manager has been configured");
}
}
}
// 管理当前开启的事务管理器
return prepareTransactionInfo(tm, txAttr, joinpointIdentification, status);
}

首先,先根据当前的事务管理级别创建 TransactionStatus

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
@Override
public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
throws TransactionException {

// 获取 TransactionDefinition,如果配置中没有定义则获取默认的事务定义.
TransactionDefinition def = (definition != null ? definition : TransactionDefinition.withDefaults());

// 获取事务状态对象
Object transaction = doGetTransaction();
boolean debugEnabled = logger.isDebugEnabled();

if (isExistingTransaction(transaction)) {
// 当前存在事务,根据不同等级返回 TransactionStatus.
return handleExistingTransaction(def, transaction, debugEnabled);
}

// 超时的话抛出异常.
if (def.getTimeout() < TransactionDefinition.TIMEOUT_DEFAULT) {
throw new InvalidTimeoutException("Invalid transaction timeout", def.getTimeout());
}

// 不存在事务的时候,需要根据上面的事务定义来继续行为.
if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) {
// 这个等级是没有事务即抛出异常
throw new IllegalTransactionStateException(
"No existing transaction found for transaction marked with propagation 'mandatory'");
}
else if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED
def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW
def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
SuspendedResourcesHolder suspendedResources = suspend(null);
if (debugEnabled) {
logger.debug("Creating new transaction with name [" + def.getName() + "]: " + def);
}
try {
// 创建新的 TransactionStatus 然后开启数据库事务
boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
// 创建一个新的DefaultTransactionStatus贯穿整个事务
DefaultTransactionStatus status = newTransactionStatus(
def, transaction, true, newSynchronization, debugEnabled, suspendedResources);
// 开启新事务
doBegin(transaction, def);
// 设置激活程序中的事务管理器,设置只读属性、事务名字、隔离级别等信息
prepareSynchronization(status, def);
return status;
}
catch (RuntimeException Error ex) {
resume(null, suspendedResources);
throw ex;
}
}
else {
// Create "empty" transaction: no actual transaction, but potentially synchronization.
if (def.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT && logger.isWarnEnabled()) {
logger.warn("Custom isolation level specified but no actual transaction initiated; " +
"isolation level will effectively be ignored: " + def);
}
boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
return prepareTransactionStatus(def, null, true, newSynchronization, debugEnabled, null);
}
}

OK,接下来看看 doBegin 怎么管理 Connection

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// 开启事务管理器
@Override
protected void doBegin(Object transaction, TransactionDefinition definition) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
Connection con = null;

try {
if (!txObject.hasConnectionHolder()
txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
// 通过数据库连接池拿到 Connection
Connection newCon = obtainDataSource().getConnection();
if (logger.isDebugEnabled()) {
logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
}
// 使用ConnectionHolder管理当前的连接
txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
}

txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
con = txObject.getConnectionHolder().getConnection();

// Connection属性设置(之前的隔离级别、是否只读)
Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
txObject.setPreviousIsolationLevel(previousIsolationLevel);
txObject.setReadOnly(definition.isReadOnly());

// 修改Connection的只读状态
if (con.getAutoCommit()) {
txObject.setMustRestoreAutoCommit(true);
if (logger.isDebugEnabled()) {
logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
}
con.setAutoCommit(false);
}

// 如果当前是只读事务则发送 SET TRANSACTION READ ONLY 给数据库切换事务状态
prepareTransactionalConnection(con, definition);
txObject.getConnectionHolder().setTransactionActive(true);

int timeout = determineTimeout(definition);
if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
}

// 把ConnectionHolder绑定到TransactionSynchronizationManager
if (txObject.isNewConnectionHolder()) {
TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());
}
}

catch (Throwable ex) {
if (txObject.isNewConnectionHolder()) {
DataSourceUtils.releaseConnection(con, obtainDataSource());
txObject.setConnectionHolder(null, false);
}
throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex);
}
}

开始执行

为了防止需要拖拉到上面去阅读,我先把之前执行到哪个地方放在这里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
@Nullable
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
final InvocationCallback invocation) throws Throwable {

// 获取所有配置的事务属性元数据类,如果为null,代表默认事务(默认是autoCommit=true)
TransactionAttributeSource tas = getTransactionAttributeSource();
// 从缓存中命中当前方法的事务配置
final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
// 获取事务管理器
final TransactionManager tm = determineTransactionManager(txAttr);

// Reactive环境走这里,先跳过
if (this.reactiveAdapterRegistry != null && tm instanceof ReactiveTransactionManager) {
ReactiveTransactionSupport txSupport = this.transactionSupportCache.computeIfAbsent(method, key -> {
if (KotlinDetector.isKotlinType(method.getDeclaringClass()) && KotlinDelegate.isSuspend(method)) {
throw new TransactionUsageException(
"Unsupported annotated transaction on suspending function detected: " + method +
". Use TransactionalOperator.transactional extensions instead.");
}
ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(method.getReturnType());
if (adapter == null) {
throw new IllegalStateException("Cannot apply reactive transaction to non-reactive return type: " +
method.getReturnType());
}
return new ReactiveTransactionSupport(adapter);
});
return txSupport.invokeWithinTransaction(
method, targetClass, invocation, txAttr, (ReactiveTransactionManager) tm);
}

// 获取Spring提供的统一事务管理器
PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);

if (txAttr == null !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
// 创建或者加入当前事务
TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);

Object retVal;
try {
// 执行目标方法,如果有下一个AOP链则接下去执行
// ---------------> 已经准备好了事务信息,可以开始执行整个AOP链条了
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// 遇到异常,回滚数据库事务
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
// 清空当前事务,如果当前是嵌套事务,管理器会重新拿到之前的事务
cleanupTransactionInfo(txInfo);
}

// 响应式数据库操作 也是先跳过
if (vavrPresent && VavrDelegate.isVavrTry(retVal)) {
// Set rollback-only in case of Vavr failure matching our rollback rules...
TransactionStatus status = txInfo.getTransactionStatus();
if (status != null && txAttr != null) {
retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
}
}

// 提交数据库事务
commitTransactionAfterReturning(txInfo);
return retVal;
}
// CallbackPreferringPlatformTransactionManager 可回调的管理器
// 也是响应式的内容
else {
// 省略响应式内容.........
}
}

正常提交

拿到事务管理器,提交事务:

1
2
3
4
5
6
7
8
protected void commitTransactionAfterReturning(@Nullable TransactionInfo txInfo) {
if (txInfo != null && txInfo.getTransactionStatus() != null) {
if (logger.isTraceEnabled()) {
logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() + "]");
}
txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
}
}

需要经过判断来做是否真正提交:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Override
public final void commit(TransactionStatus status) throws TransactionException {
if (status.isCompleted()) {
throw new IllegalTransactionStateException(
"Transaction is already completed - do not call commit or rollback more than once per transaction");
}

DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;

// 根据用户TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();设置
// 是否不让提交(比如校验没有通过,但是没有抛出异常) 如果设置了则回滚数据库
if (defStatus.isLocalRollbackOnly()) {
if (defStatus.isDebug()) {
logger.debug("Transactional code has requested rollback");
}
processRollback(defStatus, false);
return;
}

// 这里是判断全局事务中配置了只能在全局回滚并且当前已经出现错误,如果当前是内部事务,会标记业务层事务出现错误
if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) {
if (defStatus.isDebug()) {
logger.debug("Global transaction is marked as rollback-only but transactional code requested commit");
}
processRollback(defStatus, true);
return;
}

// 处理提交事务
processCommit(defStatus);
}

处理提交,可以说,我们项目中使用到提交事务生命周期都在这里被调用到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
private void processCommit(DefaultTransactionStatus status) throws TransactionException {
try {
boolean beforeCompletionInvoked = false;

try {
boolean unexpectedRollback = false;
// 下面三个是调用相对应的生命周期
prepareForCommit(status);
triggerBeforeCommit(status);
triggerBeforeCompletion(status);
beforeCompletionInvoked = true;

// SavePoint 先不看
if (status.hasSavepoint()) {
if (status.isDebug()) {
logger.debug("Releasing transaction savepoint");
}
unexpectedRollback = status.isGlobalRollbackOnly();
status.releaseHeldSavepoint();
}
// 当前是一个新的事务
else if (status.isNewTransaction()) {
if (status.isDebug()) {
logger.debug("Initiating transaction commit");
}
unexpectedRollback = status.isGlobalRollbackOnly();
// 真正做提交的地方
doCommit(status);
}
else if (isFailEarlyOnGlobalRollbackOnly()) {
unexpectedRollback = status.isGlobalRollbackOnly();
}

// Throw UnexpectedRollbackException if we have a global rollback-only
// marker but still didn't get a corresponding exception from commit.
if (unexpectedRollback) {
throw new UnexpectedRollbackException(
"Transaction silently rolled back because it has been marked as rollback-only");
}
}
catch (UnexpectedRollbackException ex) {
// can only be caused by doCommit
triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK);
throw ex;
}
catch (TransactionException ex) {
// can only be caused by doCommit
if (isRollbackOnCommitFailure()) {
doRollbackOnCommitException(status, ex);
}
else {
triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN);
}
throw ex;
}
catch (RuntimeException Error ex) {
if (!beforeCompletionInvoked) {
triggerBeforeCompletion(status);
}
doRollbackOnCommitException(status, ex);
throw ex;
}

// Trigger afterCommit callbacks, with an exception thrown there
// propagated to callers but the transaction still considered as committed.
try {
// 调用事务生命周期的后处理器
triggerAfterCommit(status);
}
finally {
triggerAfterCompletion(status, TransactionSynchronization.STATUS_COMMITTED);
}

}
finally {
cleanupAfterCompletion(status);
}
}

拿到 Connection 调用 Commit 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
protected void doCommit(DefaultTransactionStatus status) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
Connection con = txObject.getConnectionHolder().getConnection();
if (status.isDebug()) {
logger.debug("Committing JDBC transaction on Connection [" + con + "]");
}
try {
con.commit();
}
catch (SQLException ex) {
throw new TransactionSystemException("Could not commit JDBC transaction", ex);
}
}

回滚数据库

业务代码加一个除以 0 的计算:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Service
public class UserServiceImpl implements UserService {

@Autowired
private JdbcTemplate jdbcTemplate;

@Override
@Transactional
public void add(String name, String orgName) {
String userId = UUID.randomUUID().toString();
jdbcTemplate.update("insert into user_info(user_uuid, user_name)\n" +
"values (?,?);", userId, name);
int i = 1 / 0;
String orgUUID = UUID.randomUUID().toString();
jdbcTemplate.update("insert into org_info(org_uuid, org_name, user_uuid)\n" +
"VALUES (?, ?, ?);", orgUUID, orgName, userId);
}

}

那么出现了异常了,这一步会来到 try-catch 中的 catch:

1
2
3
4
5
6
7
8
9
10
11
12
try {
// 执行目标方法,如果有下一个AOP链则接下去执行
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// 遇到异常,回滚数据库事务
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
cleanupTransactionInfo(txInfo);
}

这个是简单了很多了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
if (txInfo != null && txInfo.getTransactionStatus() != null) {
if (logger.isTraceEnabled()) {
logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() +
"] after exception: " + ex);
}
// 判断当前抛出的异常是否需要回滚数据库
if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
try {
txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
}
catch (TransactionSystemException ex2) {
logger.error("Application exception overridden by rollback exception", ex);
ex2.initApplicationException(ex);
throw ex2;
}
catch (RuntimeException Error ex2) {
logger.error("Application exception overridden by rollback exception", ex);
throw ex2;
}
}
// 如果不会滚,继续提交
else {
// We don't roll back on this exception.
// Will still roll back if TransactionStatus.isRollbackOnly() is true.
try {
txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
}
catch (TransactionSystemException ex2) {
logger.error("Application exception overridden by commit exception", ex);
ex2.initApplicationException(ex);
throw ex2;
}
catch (RuntimeException Error ex2) {
logger.error("Application exception overridden by commit exception", ex);
throw ex2;
}
}
}
}

好吧来到了处理方法:

1
2
3
4
5
6
7
8
9
10
@Override
public final void rollback(TransactionStatus status) throws TransactionException {
if (status.isCompleted()) {
throw new IllegalTransactionStateException(
"Transaction is already completed - do not call commit or rollback more than once per transaction");
}

DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;
processRollback(defStatus, false);
}

那么其实跟提交的套路是一样的,触发一系列的生命周期:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
private void processRollback(DefaultTransactionStatus status, boolean unexpected) {
try {
boolean unexpectedRollback = unexpected;

try {
triggerBeforeCompletion(status);

if (status.hasSavepoint()) {
if (status.isDebug()) {
logger.debug("Rolling back transaction to savepoint");
}
status.rollbackToHeldSavepoint();
}
else if (status.isNewTransaction()) {
if (status.isDebug()) {
logger.debug("Initiating transaction rollback");
}
doRollback(status);
}
else {
// Participating in larger transaction
if (status.hasTransaction()) {
if (status.isLocalRollbackOnly() isGlobalRollbackOnParticipationFailure()) {
if (status.isDebug()) {
logger.debug("Participating transaction failed - marking existing transaction as rollback-only");
}
doSetRollbackOnly(status);
}
else {
if (status.isDebug()) {
logger.debug("Participating transaction failed - letting transaction originator decide on rollback");
}
}
}
else {
logger.debug("Should roll back transaction but cannot - no transaction available");
}
// Unexpected rollback only matters here if we're asked to fail early
if (!isFailEarlyOnGlobalRollbackOnly()) {
unexpectedRollback = false;
}
}
}
catch (RuntimeException Error ex) {
triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN);
throw ex;
}

triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK);

// Raise UnexpectedRollbackException if we had a global rollback-only marker
if (unexpectedRollback) {
throw new UnexpectedRollbackException(
"Transaction rolled back because it has been marked as rollback-only");
}
}
finally {
cleanupAfterCompletion(status);
}
}

拿到 Connection 进行回滚。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
protected void doRollback(DefaultTransactionStatus status) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
Connection con = txObject.getConnectionHolder().getConnection();
if (status.isDebug()) {
logger.debug("Rolling back JDBC transaction on Connection [" + con + "]");
}
try {
con.rollback();
}
catch (SQLException ex) {
throw new TransactionSystemException("Could not roll back JDBC transaction", ex);
}
}

简述

由于现在基本使用的都是 SpringBoot 项目来启动,所以我就直接建立在 SpringBoot 基础下看 WEB 容器了。 如果有一些项目开发经验就知道,SpringWEB 模块又名 SpringMVC,提供了视图数据整合以及可以直接返回 JSON 数据。前几年 JSP 当道的时候,各种 ViewResolver 层出不穷,据我所知的就有 FreeMarker JSP Beetl 等等。不过最近几年前端发生爆炸性变化,前后端能分离的都分离了,需要 SEO 的也很少使用 Java 来做了。所以现在 Java 后端项目的主要责职是提供业务所需要的交互数据给前端,不仅仅使用 JSON,还可以整合其他的传输协议,比如 Hessian XML Protobuf 等等,只要够折腾,什么格式都可以返回,不想折腾就使用 JSON 就可以了。 数据转换模块也会被 Spring 独立出来这个自然不需要再猜测,所以我们也可以为了减少传输使用的宽带,去定义第几个字节是什么东西。 那我们现在可以直接进入 SpringMVC 的源码来看看了。

初始化Web环境

JavaWEB 中,如果需要提供网络服务,一般是实现 Servlet 对象,像 Tomcat Resign 等网络容器都提供了自己的实现,我们就简单的用常用的 Tomcat 来说,Tomcat 会拿到我们项目中所有的 HTTPServlet 然后根据不同的 HTTP Method HTTP URL 来调用不同的 Servlet 方法来完成请求。 那 SpringMVC 是在 Servlet 级别的,所以这个容器必定会存在,那就是 DispatcherServlet。简单的理解 DispatcherServlet 就是一个在项目启动时读取了所有请求处理器,然后有请求进来根据不同的定义来调用不同的请求处理器的一个 Servlet 容器。 把断点打在 DispatcherServlet 的无参构造器上,找到初始化入口,是在 refreshContext(context) 中进行的,那我们就从这里开始:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
configureHeadlessProperty();
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
// 我们之前说过,这里会根据依赖的 jar 环境来创建不同的 Context
// 所以现在创建的是 AnnotationConfigServletWebServerApplicationContext 容器
context = createApplicationContext();
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
// 刷新容器
refreshContext(context);
afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
listeners.started(context);
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}

try {
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
return context;
}

一系列之前说过的步骤以后,来到这里:

1
2
3
4
protected void refresh(ApplicationContext applicationContext) {
Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext);
((AbstractApplicationContext) applicationContext).refresh();
}

之前说过,调用 refresh 是个模板方法,AbstractApplicationContextrefresh 是个模板方法,会调用子类实现的 onRefresh。因为当前是个 WEB 项目,所以实现类 AnnotationConfigServletWebServerApplicationContextonRefresh 提供了实现:

1
2
3
4
5
6
7
8
9
10
@Override
protected void onRefresh() {
super.onRefresh();
try {
createWebServer();
}
catch (Throwable ex) {
throw new ApplicationContextException("Unable to start web server", ex);
}
}

第一件事请 super.onRefresh() 是调用父级 GenericWebApplicationContext 的函数,不过这段函数现在看来应该没什么多大的用处了:

1
2
3
4
5
6
7
/**
* 初始化界面主题容器
*/
@Override
protected void onRefresh() {
this.themeSource = UiApplicationContextUtils.initThemeSource(this);
}

是一个初始化界面主题的函数,一开始我还怀疑这个主题是不是什么主题,然而进入 UiApplicationContextUtils.initThemeSource 看的时候,还真的就是 HTML 主题,里面定义了从哪里读取 CSS 呀这些鬼东西,跳过不看。 重点来看看 createWebServer() 做了哪些事情:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private void createWebServer() {
WebServer webServer = this.webServer;
ServletContext servletContext = getServletContext();
if (webServer == null && servletContext == null) {
// 工厂模式
ServletWebServerFactory factory = getWebServerFactory();
// 创建webServer
this.webServer = factory.getWebServer(getSelfInitializer());
}
else if (servletContext != null) {
try {
getSelfInitializer().onStartup(servletContext);
}
catch (ServletException ex) {
throw new ApplicationContextException("Cannot initialize servlet context", ex);
}
}
// 初始化配置属性
initPropertySources();
}

这时候获取了一个 ServletWebServerFactoryBeanDefinition,然后使用这个 Factory 来创建我们目前的 Web 环境。

1
2
3
4
5
6
7
8
9
10
11
12
13
protected ServletWebServerFactory getWebServerFactory() {
// Use bean names so that we don't consider the hierarchy
String[] beanNames = getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class);
if (beanNames.length == 0) {
throw new ApplicationContextException("Unable to start ServletWebServerApplicationContext due to missing "
+ "ServletWebServerFactory bean.");
}
if (beanNames.length > 1) {
throw new ApplicationContextException("Unable to start ServletWebServerApplicationContext due to multiple "
+ "ServletWebServerFactory beans : " + StringUtils.arrayToCommaDelimitedString(beanNames));
}
return getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class);
}

内置Tomcat注入

然而这个ServletWebServerFactory 什么时候偷偷被放进去的,我们现在快速过一遍:

  1. SpringBoot 启动的时候,在构造器调用了元信息读取器,读取了 META-INF/spring.factories 中配置的工厂,后初始化的时候会用到这些配置的类;
  2. SpringBoot 中上面的配置文件配置了这个工厂:ServletWebServerFactoryAutoConfiguration
  3. 看这个工厂有什么信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Configuration(proxyBeanMethods = false)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@ConditionalOnClass(ServletRequest.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(ServerProperties.class)
@Import({ ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,
ServletWebServerFactoryConfiguration.EmbeddedTomcat.class,
ServletWebServerFactoryConfiguration.EmbeddedJetty.class,
ServletWebServerFactoryConfiguration.EmbeddedUndertow.class })
public class ServletWebServerFactoryAutoConfiguration {

// 自定义容器的初始化:
@Bean
public ServletWebServerFactoryCustomizer servletWebServerFactoryCustomizer(ServerProperties serverProperties) {
return new ServletWebServerFactoryCustomizer(serverProperties);
}

@Bean
@ConditionalOnClass(name = "org.apache.catalina.startup.Tomcat")
public TomcatServletWebServerFactoryCustomizer tomcatServletWebServerFactoryCustomizer(
ServerProperties serverProperties) {
return new TomcatServletWebServerFactoryCustomizer(serverProperties);
}

// 其他容器初始化...
}

然后我们看到上面导入那里导入了内置 Tomcat

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Configuration(proxyBeanMethods = false)
class ServletWebServerFactoryConfiguration {

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class })
@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
public static class EmbeddedTomcat {

@Bean
public TomcatServletWebServerFactory tomcatServletWebServerFactory(
ObjectProvider<TomcatConnectorCustomizer> connectorCustomizers,
ObjectProvider<TomcatContextCustomizer> contextCustomizers,
ObjectProvider<TomcatProtocolHandlerCustomizer<?>> protocolHandlerCustomizers) {
TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
factory.getTomcatConnectorCustomizers()
.addAll(connectorCustomizers.orderedStream().collect(Collectors.toList()));
factory.getTomcatContextCustomizers()
.addAll(contextCustomizers.orderedStream().collect(Collectors.toList()));
factory.getTomcatProtocolHandlerCustomizers()
.addAll(protocolHandlerCustomizers.orderedStream().collect(Collectors.toList()));
return factory;
}

}
//...
}

所以这个 ServletWebServerFactory 就在读取配置的时候被自动导入到 BeanDefinitionMaps 中去。

DispatcherServlet构建

怎么会涉及这一块的初始化呢,是因为上面 ServletWebServerFactoryConfiguration 初始化完成以后,触发的 ServletWebServerFactoryConfiguration 初始化,先来看看 ServletWebServerFactoryConfiguration 的注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass(DispatcherServlet.class)
@AutoConfigureAfter(ServletWebServerFactoryAutoConfiguration.class)
public class DispatcherServletAutoConfiguration {

/*
* The bean name for a DispatcherServlet that will be mapped to the root URL "/"
*/
public static final String DEFAULT_DISPATCHER_SERVLET_BEAN_NAME = "dispatcherServlet";

/*
* The bean name for a ServletRegistrationBean for the DispatcherServlet "/"
*/
public static final String DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME = "dispatcherServletRegistration";

@Configuration(proxyBeanMethods = false)
@Conditional(DefaultDispatcherServletCondition.class)
@ConditionalOnClass(ServletRegistration.class)
@EnableConfigurationProperties({ HttpProperties.class, WebMvcProperties.class })
protected static class DispatcherServletConfiguration {

@Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public DispatcherServlet dispatcherServlet(HttpProperties httpProperties, WebMvcProperties webMvcProperties) {
DispatcherServlet dispatcherServlet = new DispatcherServlet();
dispatcherServlet.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest());
dispatcherServlet.setDispatchTraceRequest(webMvcProperties.isDispatchTraceRequest());
dispatcherServlet.setThrowExceptionIfNoHandlerFound(webMvcProperties.isThrowExceptionIfNoHandlerFound());
dispatcherServlet.setPublishEvents(webMvcProperties.isPublishRequestHandledEvents());
dispatcherServlet.setEnableLoggingRequestDetails(httpProperties.isLogRequestDetails());
return dispatcherServlet;
}

@Bean
@ConditionalOnBean(MultipartResolver.class)
@ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)
public MultipartResolver multipartResolver(MultipartResolver resolver) {
// Detect if the user has created a MultipartResolver but named it incorrectly
return resolver;
}

}
// 其他暂时跳过的配置
}

这个初始化就很有灵性了,跟我们平常写的配置类大致一致。那关于 DispatcherServlet 的配置,我们均可以通过配置 WebMvcProperties 来让他注入到当前的 DispatcherServlet。 这就完了😱?映射呢映射呢? 好吧,差点忘记此时 HTTPServlet 还没有初始化。

DispatcherServlet初始化(一)

默认的 HTTPServlet 在启动的时候只有装载,并没有初始化,初始化发生在第一次请求的时候。 我们来看看这个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public abstract class HttpServletBean extends HttpServlet implements EnvironmentCapable, EnvironmentAware {
@Override
public final void init() throws ServletException {

// 读取配置.
PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
if (!pvs.isEmpty()) {
try {
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
initBeanWrapper(bw);
bw.setPropertyValues(pvs, true);
}
catch (BeansException ex) {
if (logger.isErrorEnabled()) {
logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
}
throw ex;
}
}

// 子类实现初始化.
initServletBean();
}
}

FrameworkServlet 实现上面的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
@Override
protected final void initServletBean() throws ServletException {
getServletContext().log("Initializing Spring " + getClass().getSimpleName() + " '" + getServletName() + "'");
if (logger.isInfoEnabled()) {
logger.info("Initializing Servlet '" + getServletName() + "'");
}
long startTime = System.currentTimeMillis();

try {
// 初始化Web应用的上下文
this.webApplicationContext = initWebApplicationContext();
// 初始化
initFrameworkServlet();
}
catch (ServletException RuntimeException ex) {
logger.error("Context initialization failed", ex);
throw ex;
}

if (logger.isDebugEnabled()) {
String value = this.enableLoggingRequestDetails ?
"shown which may lead to unsafe logging of potentially sensitive data" :
"masked to prevent unsafe logging of potentially sensitive data";
logger.debug("enableLoggingRequestDetails='" + this.enableLoggingRequestDetails +
"': request parameters and headers will be " + value);
}

if (logger.isInfoEnabled()) {
logger.info("Completed initialization in " + (System.currentTimeMillis() - startTime) + " ms");
}
}
protected WebApplicationContext initWebApplicationContext() {
WebApplicationContext rootContext =
WebApplicationContextUtils.getWebApplicationContext(getServletContext());
WebApplicationContext wac = null;

if (this.webApplicationContext != null) {
// 当前rootContext已经存在了
wac = this.webApplicationContext;
if (wac instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
// 已经激活,这里并不会进来
if (!cwac.isActive()) {
// The context has not yet been refreshed -> provide services such as
// setting the parent context, setting the application context id, etc
if (cwac.getParent() == null) {
// The context instance was injected without an explicit parent -> set
// the root application context (if any; may be null) as the parent
cwac.setParent(rootContext);
}
configureAndRefreshWebApplicationContext(cwac);
}
}
}
if (wac == null) {
// No context instance was injected at construction time -> see if one
// has been registered in the servlet context. If one exists, it is assumed
// that the parent context (if any) has already been set and that the
// user has performed any initialization such as setting the context id
wac = findWebApplicationContext();
}
if (wac == null) {
// No context instance is defined for this servlet -> create a local one
wac = createWebApplicationContext(rootContext);
}

if (!this.refreshEventReceived) {
// 刷新上下文
synchronized (this.onRefreshMonitor) {
onRefresh(wac);
}
}

if (this.publishContext) {
// Publish the context as a servlet context attribute.
String attrName = getServletContextAttributeName();
getServletContext().setAttribute(attrName, wac);
}

return wac;
}

刷新上下文在上面那么多个容器中已经很有感觉了,就是刷新配置的 Bean 调用对应的构造器 AOP 注入啊啊巴拉巴拉。 所以,onRefresh 是整个 Web 容器初始化的关键:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
DispatcherServlet.java:
@Override
protected void onRefresh(ApplicationContext context) {
initStrategies(context);
}
// 初始化DispatcherServlet所有的内容
protected void initStrategies(ApplicationContext context) {
// 初始化多媒体处理器,处理上传的请求
initMultipartResolver(context);
// i18n初始化
initLocaleResolver(context);
// 项目主题
initThemeResolver(context);
// 请求处理器,如果我们写Controller应该是RequestMappingHandlerMapping处理
initHandlerMappings(context);
// 初始化适配器,会在调用的时候寻找合适的控制器和拦截器整合一起调用
initHandlerAdapters(context);
// 异常处理器
initHandlerExceptionResolvers(context);
// 视图名字处理器,通过控制器返回的字符串查找到对应的视图
initRequestToViewNameTranslator(context);
// 视图处理器,处理返回值与界面的整合
initViewResolvers(context);
// 初始化FlashMap处理器,FlashMap是一个保存数据的地方,
// 在重定向的时候会把参数临时存储在session中
// 跳转完成即清理上一个url携带的数据
initFlashMapManager(context);
}

我大概只对 initHandlerMappingsinitHandlerAdapters 感兴趣。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
private void initHandlerMappings(ApplicationContext context) {
this.handlerMappings = null;

if (this.detectAllHandlerMappings) {
// 从BF中获取所有的HandlerMapping实例进行重组排序
Map<String, HandlerMapping> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
if (!matchingBeans.isEmpty()) {
this.handlerMappings = new ArrayList<>(matchingBeans.values());
// We keep HandlerMappings in sorted order.
AnnotationAwareOrderComparator.sort(this.handlerMappings);
}
}
else {
try {
HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
this.handlerMappings = Collections.singletonList(hm);
}
catch (NoSuchBeanDefinitionException ex) {
// Ignore, we'll add a default HandlerMapping later.
}
}

if (this.handlerMappings == null) {
this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
if (logger.isTraceEnabled()) {
logger.trace("No HandlerMappings declared for servlet '" + getServletName() +
"': using default strategies from DispatcherServlet.properties");
}
}
}

那么这些 handlerMappings 是什么时候被装配的,通过 WebMvcAutoConfiguration 这个配置类自动装配的,并且装配发生在 DispatcherServletAutoConfiguration 之后。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {

// 重写父类createRequestMappingHandlerMapping,带有@Bean注解
@Override
protected RequestMappingHandlerMapping createRequestMappingHandlerMapping() {
if (this.mvcRegistrations != null && this.mvcRegistrations.getRequestMappingHandlerMapping() != null) {
return this.mvcRegistrations.getRequestMappingHandlerMapping();
}
return super.createRequestMappingHandlerMapping();
}

}

RequestMappingHandlerMapping装配

上面我们看到,RequestMappingHandler 是通过自动装配的形式放入 BeanFactory 的,那至于我们编写的 @Controller 以及 @RestController 是怎么被读取的,就是通过装配时,调用 InitializingBean 接口的 afterPropertiesSet 方法进行装配的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// RequestMappingHandlerMapping.java
@Override
public void afterPropertiesSet() {
this.config = new RequestMappingInfo.BuilderConfiguration();
this.config.setUrlPathHelper(getUrlPathHelper());
this.config.setPathMatcher(getPathMatcher());
this.config.setSuffixPatternMatch(this.useSuffixPatternMatch);
this.config.setTrailingSlashMatch(this.useTrailingSlashMatch);
this.config.setRegisteredSuffixPatternMatch(this.useRegisteredSuffixPatternMatch);
this.config.setContentNegotiationManager(getContentNegotiationManager());

super.afterPropertiesSet();
}
// AbstractHandlerMethodMapping.java
@Override
public void afterPropertiesSet() {
initHandlerMethods();
}
protected void initHandlerMethods() {
for (String beanName : getCandidateBeanNames()) {
if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
processCandidateBean(beanName);
}
}
handlerMethodsInitialized(getHandlerMethods());
}

拿到 BeanFactory 中所有的 Bean 一顿循环😶:

简述

由于现在基本使用的都是 SpringBoot 项目来启动,所以我就直接建立在 SpringBoot 基础下看 WEB 容器了。 如果有一些项目开发经验就知道,SpringWEB 模块又名 SpringMVC,提供了视图数据整合以及可以直接返回 JSON 数据。前几年 JSP 当道的时候,各种 ViewResolver 层出不穷,据我所知的就有 FreeMarker JSP Beetl 等等。不过最近几年前端发生爆炸性变化,前后端能分离的都分离了,需要 SEO 的也很少使用 Java 来做了。所以现在 Java 后端项目的主要责职是提供业务所需要的交互数据给前端,不仅仅使用 JSON,还可以整合其他的传输协议,比如 Hessian XML Protobuf 等等,只要够折腾,什么格式都可以返回,不想折腾就使用 JSON 就可以了。 数据转换模块也会被 Spring 独立出来这个自然不需要再猜测,所以我们也可以为了减少传输使用的宽带,去定义第几个字节是什么东西。 那我们现在可以直接进入 SpringMVC 的源码来看看了。

初始化Web环境

JavaWEB 中,如果需要提供网络服务,一般是实现 Servlet 对象,像 Tomcat Resign 等网络容器都提供了自己的实现,我们就简单的用常用的 Tomcat 来说,Tomcat 会拿到我们项目中所有的 HTTPServlet 然后根据不同的 HTTP Method HTTP URL 来调用不同的 Servlet 方法来完成请求。 那 SpringMVC 是在 Servlet 级别的,所以这个容器必定会存在,那就是 DispatcherServlet。简单的理解 DispatcherServlet 就是一个在项目启动时读取了所有请求处理器,然后有请求进来根据不同的定义来调用不同的请求处理器的一个 Servlet 容器。 把断点打在 DispatcherServlet 的无参构造器上,找到初始化入口,是在 refreshContext(context) 中进行的,那我们就从这里开始:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
configureHeadlessProperty();
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
// 我们之前说过,这里会根据依赖的 jar 环境来创建不同的 Context
// 所以现在创建的是 AnnotationConfigServletWebServerApplicationContext 容器
context = createApplicationContext();
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
// 刷新容器
refreshContext(context);
afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
listeners.started(context);
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}

try {
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
return context;
}

一系列之前说过的步骤以后,来到这里:

1
2
3
4
protected void refresh(ApplicationContext applicationContext) {
Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext);
((AbstractApplicationContext) applicationContext).refresh();
}

之前说过,调用 refresh 是个模板方法,AbstractApplicationContextrefresh 是个模板方法,会调用子类实现的 onRefresh。因为当前是个 WEB 项目,所以实现类 AnnotationConfigServletWebServerApplicationContextonRefresh 提供了实现:

1
2
3
4
5
6
7
8
9
10
@Override
protected void onRefresh() {
super.onRefresh();
try {
createWebServer();
}
catch (Throwable ex) {
throw new ApplicationContextException("Unable to start web server", ex);
}
}

第一件事请 super.onRefresh() 是调用父级 GenericWebApplicationContext 的函数,不过这段函数现在看来应该没什么多大的用处了:

1
2
3
4
5
6
7
/**
* 初始化界面主题容器
*/
@Override
protected void onRefresh() {
this.themeSource = UiApplicationContextUtils.initThemeSource(this);
}

是一个初始化界面主题的函数,一开始我还怀疑这个主题是不是什么主题,然而进入 UiApplicationContextUtils.initThemeSource 看的时候,还真的就是 HTML 主题,里面定义了从哪里读取 CSS 呀这些鬼东西,跳过不看。 重点来看看 createWebServer() 做了哪些事情:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private void createWebServer() {
WebServer webServer = this.webServer;
ServletContext servletContext = getServletContext();
if (webServer == null && servletContext == null) {
// 工厂模式
ServletWebServerFactory factory = getWebServerFactory();
// 创建webServer
this.webServer = factory.getWebServer(getSelfInitializer());
}
else if (servletContext != null) {
try {
getSelfInitializer().onStartup(servletContext);
}
catch (ServletException ex) {
throw new ApplicationContextException("Cannot initialize servlet context", ex);
}
}
// 初始化配置属性
initPropertySources();
}

这时候获取了一个 ServletWebServerFactoryBeanDefinition,然后使用这个 Factory 来创建我们目前的 Web 环境。

1
2
3
4
5
6
7
8
9
10
11
12
13
protected ServletWebServerFactory getWebServerFactory() {
// Use bean names so that we don't consider the hierarchy
String[] beanNames = getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class);
if (beanNames.length == 0) {
throw new ApplicationContextException("Unable to start ServletWebServerApplicationContext due to missing "
+ "ServletWebServerFactory bean.");
}
if (beanNames.length > 1) {
throw new ApplicationContextException("Unable to start ServletWebServerApplicationContext due to multiple "
+ "ServletWebServerFactory beans : " + StringUtils.arrayToCommaDelimitedString(beanNames));
}
return getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class);
}

内置Tomcat注入

然而这个ServletWebServerFactory 什么时候偷偷被放进去的,我们现在快速过一遍:

  1. SpringBoot 启动的时候,在构造器调用了元信息读取器,读取了 META-INF/spring.factories 中配置的工厂,后初始化的时候会用到这些配置的类;
  2. SpringBoot 中上面的配置文件配置了这个工厂:ServletWebServerFactoryAutoConfiguration
  3. 看这个工厂有什么信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Configuration(proxyBeanMethods = false)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@ConditionalOnClass(ServletRequest.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(ServerProperties.class)
@Import({ ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,
ServletWebServerFactoryConfiguration.EmbeddedTomcat.class,
ServletWebServerFactoryConfiguration.EmbeddedJetty.class,
ServletWebServerFactoryConfiguration.EmbeddedUndertow.class })
public class ServletWebServerFactoryAutoConfiguration {

// 自定义容器的初始化:
@Bean
public ServletWebServerFactoryCustomizer servletWebServerFactoryCustomizer(ServerProperties serverProperties) {
return new ServletWebServerFactoryCustomizer(serverProperties);
}

@Bean
@ConditionalOnClass(name = "org.apache.catalina.startup.Tomcat")
public TomcatServletWebServerFactoryCustomizer tomcatServletWebServerFactoryCustomizer(
ServerProperties serverProperties) {
return new TomcatServletWebServerFactoryCustomizer(serverProperties);
}

// 其他容器初始化...
}

然后我们看到上面导入那里导入了内置 Tomcat

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Configuration(proxyBeanMethods = false)
class ServletWebServerFactoryConfiguration {

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class })
@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
public static class EmbeddedTomcat {

@Bean
public TomcatServletWebServerFactory tomcatServletWebServerFactory(
ObjectProvider<TomcatConnectorCustomizer> connectorCustomizers,
ObjectProvider<TomcatContextCustomizer> contextCustomizers,
ObjectProvider<TomcatProtocolHandlerCustomizer<?>> protocolHandlerCustomizers) {
TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
factory.getTomcatConnectorCustomizers()
.addAll(connectorCustomizers.orderedStream().collect(Collectors.toList()));
factory.getTomcatContextCustomizers()
.addAll(contextCustomizers.orderedStream().collect(Collectors.toList()));
factory.getTomcatProtocolHandlerCustomizers()
.addAll(protocolHandlerCustomizers.orderedStream().collect(Collectors.toList()));
return factory;
}

}
//...
}

所以这个 ServletWebServerFactory 就在读取配置的时候被自动导入到 BeanDefinitionMaps 中去。

DispatcherServlet构建

怎么会涉及这一块的初始化呢,是因为上面 ServletWebServerFactoryConfiguration 初始化完成以后,触发的 ServletWebServerFactoryConfiguration 初始化,先来看看 ServletWebServerFactoryConfiguration 的注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass(DispatcherServlet.class)
@AutoConfigureAfter(ServletWebServerFactoryAutoConfiguration.class)
public class DispatcherServletAutoConfiguration {

/*
* The bean name for a DispatcherServlet that will be mapped to the root URL "/"
*/
public static final String DEFAULT_DISPATCHER_SERVLET_BEAN_NAME = "dispatcherServlet";

/*
* The bean name for a ServletRegistrationBean for the DispatcherServlet "/"
*/
public static final String DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME = "dispatcherServletRegistration";

@Configuration(proxyBeanMethods = false)
@Conditional(DefaultDispatcherServletCondition.class)
@ConditionalOnClass(ServletRegistration.class)
@EnableConfigurationProperties({ HttpProperties.class, WebMvcProperties.class })
protected static class DispatcherServletConfiguration {

@Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public DispatcherServlet dispatcherServlet(HttpProperties httpProperties, WebMvcProperties webMvcProperties) {
DispatcherServlet dispatcherServlet = new DispatcherServlet();
dispatcherServlet.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest());
dispatcherServlet.setDispatchTraceRequest(webMvcProperties.isDispatchTraceRequest());
dispatcherServlet.setThrowExceptionIfNoHandlerFound(webMvcProperties.isThrowExceptionIfNoHandlerFound());
dispatcherServlet.setPublishEvents(webMvcProperties.isPublishRequestHandledEvents());
dispatcherServlet.setEnableLoggingRequestDetails(httpProperties.isLogRequestDetails());
return dispatcherServlet;
}

@Bean
@ConditionalOnBean(MultipartResolver.class)
@ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)
public MultipartResolver multipartResolver(MultipartResolver resolver) {
// Detect if the user has created a MultipartResolver but named it incorrectly
return resolver;
}

}
// 其他暂时跳过的配置
}

这个初始化就很有灵性了,跟我们平常写的配置类大致一致。那关于 DispatcherServlet 的配置,我们均可以通过配置 WebMvcProperties 来让他注入到当前的 DispatcherServlet。 这就完了😱?映射呢映射呢? 好吧,差点忘记此时 HTTPServlet 还没有初始化。

DispatcherServlet初始化(一)

默认的 HTTPServlet 在启动的时候只有装载,并没有初始化,初始化发生在第一次请求的时候。 我们来看看这个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public abstract class HttpServletBean extends HttpServlet implements EnvironmentCapable, EnvironmentAware {
@Override
public final void init() throws ServletException {

// 读取配置.
PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
if (!pvs.isEmpty()) {
try {
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
initBeanWrapper(bw);
bw.setPropertyValues(pvs, true);
}
catch (BeansException ex) {
if (logger.isErrorEnabled()) {
logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
}
throw ex;
}
}

// 子类实现初始化.
initServletBean();
}
}

FrameworkServlet 实现上面的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
@Override
protected final void initServletBean() throws ServletException {
getServletContext().log("Initializing Spring " + getClass().getSimpleName() + " '" + getServletName() + "'");
if (logger.isInfoEnabled()) {
logger.info("Initializing Servlet '" + getServletName() + "'");
}
long startTime = System.currentTimeMillis();

try {
// 初始化Web应用的上下文
this.webApplicationContext = initWebApplicationContext();
// 初始化
initFrameworkServlet();
}
catch (ServletException RuntimeException ex) {
logger.error("Context initialization failed", ex);
throw ex;
}

if (logger.isDebugEnabled()) {
String value = this.enableLoggingRequestDetails ?
"shown which may lead to unsafe logging of potentially sensitive data" :
"masked to prevent unsafe logging of potentially sensitive data";
logger.debug("enableLoggingRequestDetails='" + this.enableLoggingRequestDetails +
"': request parameters and headers will be " + value);
}

if (logger.isInfoEnabled()) {
logger.info("Completed initialization in " + (System.currentTimeMillis() - startTime) + " ms");
}
}
protected WebApplicationContext initWebApplicationContext() {
WebApplicationContext rootContext =
WebApplicationContextUtils.getWebApplicationContext(getServletContext());
WebApplicationContext wac = null;

if (this.webApplicationContext != null) {
// 当前rootContext已经存在了
wac = this.webApplicationContext;
if (wac instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
// 已经激活,这里并不会进来
if (!cwac.isActive()) {
// The context has not yet been refreshed -> provide services such as
// setting the parent context, setting the application context id, etc
if (cwac.getParent() == null) {
// The context instance was injected without an explicit parent -> set
// the root application context (if any; may be null) as the parent
cwac.setParent(rootContext);
}
configureAndRefreshWebApplicationContext(cwac);
}
}
}
if (wac == null) {
// No context instance was injected at construction time -> see if one
// has been registered in the servlet context. If one exists, it is assumed
// that the parent context (if any) has already been set and that the
// user has performed any initialization such as setting the context id
wac = findWebApplicationContext();
}
if (wac == null) {
// No context instance is defined for this servlet -> create a local one
wac = createWebApplicationContext(rootContext);
}

if (!this.refreshEventReceived) {
// 刷新上下文
synchronized (this.onRefreshMonitor) {
onRefresh(wac);
}
}

if (this.publishContext) {
// Publish the context as a servlet context attribute.
String attrName = getServletContextAttributeName();
getServletContext().setAttribute(attrName, wac);
}

return wac;
}

刷新上下文在上面那么多个容器中已经很有感觉了,就是刷新配置的 Bean 调用对应的构造器 AOP 注入啊啊巴拉巴拉。 所以,onRefresh 是整个 Web 容器初始化的关键:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
DispatcherServlet.java:
@Override
protected void onRefresh(ApplicationContext context) {
initStrategies(context);
}
// 初始化DispatcherServlet所有的内容
protected void initStrategies(ApplicationContext context) {
// 初始化多媒体处理器,处理上传的请求
initMultipartResolver(context);
// i18n初始化
initLocaleResolver(context);
// 项目主题
initThemeResolver(context);
// 请求处理器,如果我们写Controller应该是RequestMappingHandlerMapping处理
initHandlerMappings(context);
// 初始化适配器,会在调用的时候寻找合适的控制器和拦截器整合一起调用
initHandlerAdapters(context);
// 异常处理器
initHandlerExceptionResolvers(context);
// 视图名字处理器,通过控制器返回的字符串查找到对应的视图
initRequestToViewNameTranslator(context);
// 视图处理器,处理返回值与界面的整合
initViewResolvers(context);
// 初始化FlashMap处理器,FlashMap是一个保存数据的地方,
// 在重定向的时候会把参数临时存储在session中
// 跳转完成即清理上一个url携带的数据
initFlashMapManager(context);
}

我大概只对 initHandlerMappingsinitHandlerAdapters 感兴趣。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
private void initHandlerMappings(ApplicationContext context) {
this.handlerMappings = null;

if (this.detectAllHandlerMappings) {
// 从BF中获取所有的HandlerMapping实例进行重组排序
Map<String, HandlerMapping> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
if (!matchingBeans.isEmpty()) {
this.handlerMappings = new ArrayList<>(matchingBeans.values());
// We keep HandlerMappings in sorted order.
AnnotationAwareOrderComparator.sort(this.handlerMappings);
}
}
else {
try {
HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
this.handlerMappings = Collections.singletonList(hm);
}
catch (NoSuchBeanDefinitionException ex) {
// Ignore, we'll add a default HandlerMapping later.
}
}

if (this.handlerMappings == null) {
this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
if (logger.isTraceEnabled()) {
logger.trace("No HandlerMappings declared for servlet '" + getServletName() +
"': using default strategies from DispatcherServlet.properties");
}
}
}

那么这些 handlerMappings 是什么时候被装配的,通过 WebMvcAutoConfiguration 这个配置类自动装配的,并且装配发生在 DispatcherServletAutoConfiguration 之后。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {

// 重写父类createRequestMappingHandlerMapping,带有@Bean注解
@Override
protected RequestMappingHandlerMapping createRequestMappingHandlerMapping() {
if (this.mvcRegistrations != null && this.mvcRegistrations.getRequestMappingHandlerMapping() != null) {
return this.mvcRegistrations.getRequestMappingHandlerMapping();
}
return super.createRequestMappingHandlerMapping();
}

}

RequestMappingHandlerMapping装配

上面我们看到,RequestMappingHandler 是通过自动装配的形式放入 BeanFactory 的,那至于我们编写的 @Controller 以及 @RestController 是怎么被读取的,就是通过装配时,调用 InitializingBean 接口的 afterPropertiesSet 方法进行装配的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// RequestMappingHandlerMapping.java
@Override
public void afterPropertiesSet() {
this.config = new RequestMappingInfo.BuilderConfiguration();
this.config.setUrlPathHelper(getUrlPathHelper());
this.config.setPathMatcher(getPathMatcher());
this.config.setSuffixPatternMatch(this.useSuffixPatternMatch);
this.config.setTrailingSlashMatch(this.useTrailingSlashMatch);
this.config.setRegisteredSuffixPatternMatch(this.useRegisteredSuffixPatternMatch);
this.config.setContentNegotiationManager(getContentNegotiationManager());

super.afterPropertiesSet();
}
// AbstractHandlerMethodMapping.java
@Override
public void afterPropertiesSet() {
initHandlerMethods();
}
protected void initHandlerMethods() {
for (String beanName : getCandidateBeanNames()) {
if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
processCandidateBean(beanName);
}
}
handlerMethodsInitialized(getHandlerMethods());
}

拿到 BeanFactory 中所有的 Bean 一顿循环😶:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// AbstractHandlerMethodMapping.java
protected void processCandidateBean(String beanName) {
Class<?> beanType = null;
try {
beanType = obtainApplicationContext().getType(beanName);
}
catch (Throwable ex) {
// An unresolvable bean type, probably from a lazy bean - let's ignore it.
if (logger.isTraceEnabled()) {
logger.trace("Could not resolve type for bean '" + beanName + "'", ex);
}
}
// 关键是 isHandler,是处理器就注册
if (beanType != null && isHandler(beanType)) {
detectHandlerMethods(beanName);
}
}

Bean@Controller 或者 @RequestMapping 就判定是一个控制器:

1
2
3
4
5
6
// isHandler: 
@Override
protected boolean isHandler(Class<?> beanType) {
return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class)
AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
}

注册处理器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// AbstractHandlerMethodMapping.java
protected void detectHandlerMethods(Object handler) {
Class<?> handlerType = (handler instanceof String ?
obtainApplicationContext().getType((String) handler) : handler.getClass());

if (handlerType != null) {
Class<?> userType = ClassUtils.getUserClass(handlerType);
// 获取处理方法和对应映射的url
// 处理映射url的时候还关系到配置的其他Path的混合等等
Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
(MethodIntrospector.MetadataLookup<T>) method -> {
try {
return getMappingForMethod(method, userType);
}
catch (Throwable ex) {
throw new IllegalStateException("Invalid mapping on handler class [" +
userType.getName() + "]: " + method, ex);
}
});
if (logger.isTraceEnabled()) {
logger.trace(formatMappings(userType, methods));
}
// 循环处理器和url,注册到当前容器
methods.forEach((method, mapping) -> {
// 获取代理方法,如果加上@Validate就会用这里,但是目前没有,先不看
Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
registerHandlerMethod(handler, invocableMethod, mapping);
});
}
}

接下来我们看注册控制器,Spring 又定义了一个 Mapping 容器,用于注册 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// RequestMappingHandlerMapping.java:
@Override
protected void registerHandlerMethod(Object handler, Method method, RequestMappingInfo mapping) {
super.registerHandlerMethod(handler, method, mapping);
// 然后根据情况,消费者的协议如果定义就更新条件
updateConsumesCondition(mapping, method);
}
// AbstractHandlerMethodMapping.java
protected void registerHandlerMethod(Object handler, Method method, T mapping) {
// 向下面定义的控制器容器注册处理器.
this.mappingRegistry.register(mapping, handler, method);
}
/**
* 一个持有所有控制器的容器
*/
class MappingRegistry {

private final Map<T, MappingRegistration<T>> registry = new HashMap<>();

private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap<>();

// 通过url查找对应的处理器
private final MultiValueMap<String, T> urlLookup = new LinkedMultiValueMap<>();

private final Map<String, List<HandlerMethod>> nameLookup = new ConcurrentHashMap<>();

private final Map<HandlerMethod, CorsConfiguration> corsLookup = new ConcurrentHashMap<>();

private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

//..
}

DispatcherServlet装配(二)

好了,在上面的装配中插入了 RequestMappingHandlerMapping 的装配,现在回来继续看两个方法:initHandlerMappingsinitHandlerAdapters

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
private void initHandlerMappings(ApplicationContext context) {
this.handlerMappings = null;

if (this.detectAllHandlerMappings) {
// Find all HandlerMappings in the ApplicationContext, including ancestor contexts.
Map<String, HandlerMapping> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
if (!matchingBeans.isEmpty()) {
// 把 handlerMappings 缓存在 DispatcherServlet
this.handlerMappings = new ArrayList<>(matchingBeans.values());
// We keep HandlerMappings in sorted order.
AnnotationAwareOrderComparator.sort(this.handlerMappings);
}
}
else {
try {
HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
this.handlerMappings = Collections.singletonList(hm);
}
catch (NoSuchBeanDefinitionException ex) {
// Ignore, we'll add a default HandlerMapping later.
}
}

// Ensure we have at least one HandlerMapping, by registering
// a default HandlerMapping if no other mappings are found.
if (this.handlerMappings == null) {
this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
if (logger.isTraceEnabled()) {
logger.trace("No HandlerMappings declared for servlet '" + getServletName() +
"': using default strategies from DispatcherServlet.properties");
}
}
}

initHandlerAdapters 也差不多代码就不贴上来了。

处理HTTP请求准备

HTTPServlet 处理请求是在 service 方法中处理的,所以现在我们需要先进入核心的这个方法中去看: FrameworkServlet 即这个 HTTPServlet 的第一个实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* Delegate GET requests to processRequest/doService.
* <p>Will also be invoked by HttpServlet's default implementation of {@code doHead},
* with a {@code NoBodyResponse} that just captures the content length.
* @see #doService
* @see #doHead
*/
@Override
protected final void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {

processRequest(request, response);
}

/**
* Delegate POST requests to {@link #processRequest}.
* @see #doService
*/
@Override
protected final void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {

processRequest(request, response);
}

/**
* Delegate PUT requests to {@link #processRequest}.
* @see #doService
*/
@Override
protected final void doPut(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {

processRequest(request, response);
}

/**
* Delegate DELETE requests to {@link #processRequest}.
* @see #doService
*/
@Override
protected final void doDelete(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {

processRequest(request, response);
}

那么其实在 FrameworkServlet 中,每一个 doXXX 都是交给 processRequest(request, response); 来做处理,所以现在我们主要看这个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {

long startTime = System.currentTimeMillis();
Throwable failureCause = null;

LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
LocaleContext localeContext = buildLocaleContext(request);

RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);

// 当我们控制器方法是一个DeferredResult的时候会用到异步Web管理器,不过目前我们没有先跳过这部分
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());

// RequestContextHolder初始化,将当前请求的参数设置到里面,这样我们就可以在任何地方拿到请求信息
initContextHolders(request, localeContext, requestAttributes);

try {
// 在子类实现的部分
doService(request, response);
}
catch (ServletException IOException ex) {
failureCause = ex;
throw ex;
}
catch (Throwable ex) {
failureCause = ex;
throw new NestedServletException("Request processing failed", ex);
}

finally {
// 清理RequestContextHolder
resetContextHolders(request, previousLocaleContext, previousAttributes);
if (requestAttributes != null) {
requestAttributes.requestCompleted();
}
logResult(request, response, failureCause, asyncManager);
// 发布请求事件,这里可以用来做操作记录吧,无论成功还是失败都能够拿到
publishRequestHandledEvent(request, response, startTime, failureCause);
}
}

查找控制器进行处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
logRequest(request);

// 保存请求快照,下面恢复可以使用
Map<String, Object> attributesSnapshot = null;
if (WebUtils.isIncludeRequest(request)) {
attributesSnapshot = new HashMap<>();
Enumeration<?> attrNames = request.getAttributeNames();
while (attrNames.hasMoreElements()) {
String attrName = (String) attrNames.nextElement();
if (this.cleanupAfterInclude attrName.startsWith(DEFAULT_STRATEGIES_PREFIX)) {
attributesSnapshot.put(attrName, request.getAttribute(attrName));
}
}
}

// 一些框架的设置,设置到Request中以供用户控制器使用
request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());

// 保存flashMap
if (this.flashMapManager != null) {
FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
if (inputFlashMap != null) {
request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
}
request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
}

try {
// 开始查找执行的控制器
doDispatch(request, response);
}
finally {
if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
// Restore the original attribute snapshot, in case of an include.
if (attributesSnapshot != null) {
restoreAttributesAfterInclude(request, attributesSnapshot);
}
}
}
}

doDispatch 就是查找控制器,并且整合拦截器调用的过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;

WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

try {
ModelAndView mv = null;
Exception dispatchException = null;

try {
// 判断请求是否是上传的请求
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);

// 查找当前请求的处理器
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}

// 查找适配器,用于整合一些拦截器等等
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

// 处理 last-modified 请求头
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}

if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}

// 调用用户的控制器以及整合的拦截器处理请求,这时候请求会被写到Response中
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

if (asyncManager.isConcurrentHandlingStarted()) {
return;
}

// 处理视图,但是目前是JSON,是个空处理
applyDefaultViewName(processedRequest, mv);
// 执行其他的拦截器周期
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}

目前的拦截器链有四个:

  1. RequestMappingHandlerAdapter
  2. HandlerFunctionAdapter
  3. HttpRequestHandlerAdapter
  4. SimpleControllerHandlerAdapter

在查找的过程中,返回第一个匹配的适配器:

1
2
3
4
5
6
7
8
9
10
11
protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
if (this.handlerAdapters != null) {
for (HandlerAdapter adapter : this.handlerAdapters) {
if (adapter.supports(handler)) {
return adapter;
}
}
}
throw new ServletException("No adapter for handler [" + handler +
"]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
}

那么我们现在拿到的实例就是 RequestMappingHandlerAdapter: 然后看看调用 mv = ha.handle(processedRequest, response, mappedHandler.getHandler()) 做什么什么事情:

1
2
3
4
5
6
7
@Override
@Nullable
public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {

return handleInternal(request, response, (HandlerMethod) handler);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Override
protected ModelAndView handleInternal(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {

ModelAndView mav;
checkRequest(request);

// 并发操作,查询是否需要加锁处理
if (this.synchronizeOnSession) {
HttpSession session = request.getSession(false);
if (session != null) {
Object mutex = WebUtils.getSessionMutex(session);
synchronized (mutex) {
mav = invokeHandlerMethod(request, response, handlerMethod);
}
}
else {
// No HttpSession available -> no mutex necessary
mav = invokeHandlerMethod(request, response, handlerMethod);
}
}
else {
// 没有Session需要同步,直接走这里
mav = invokeHandlerMethod(request, response, handlerMethod);
}

if (!response.containsHeader(HEADER_CACHE_CONTROL)) {
if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers);
}
else {
prepareResponse(response);
}
}

return mav;
}

构建请求处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {

// 将request和response封装成ServletWebRequest对象
ServletWebRequest webRequest = new ServletWebRequest(request, response);
try {
// 获取WebDataBinderFactory工厂处理表单多选项的数据绑定问题
// 如CheckBox需要绑定到对象的集合中
WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
// 处理参数为Model方法的工厂
ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);

// 设置参数处理器,包括{PATH}、RequestParam等的处理,以及返回值转换处理器
ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
if (this.argumentResolvers != null) {
invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}
if (this.returnValueHandlers != null) {
invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
}
invocableMethod.setDataBinderFactory(binderFactory);
invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);

// 初始化执行容器 ModelAndViewContainer
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
modelFactory.initModel(webRequest, mavContainer, invocableMethod);
mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);

AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);
asyncWebRequest.setTimeout(this.asyncRequestTimeout);

WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.setTaskExecutor(this.taskExecutor);
asyncManager.setAsyncWebRequest(asyncWebRequest);
asyncManager.registerCallableInterceptors(this.callableInterceptors);
asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors);

// 异步处理器的处理
if (asyncManager.hasConcurrentResult()) {
Object result = asyncManager.getConcurrentResult();
mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0];
asyncManager.clearConcurrentResult();
LogFormatUtils.traceDebug(logger, traceOn -> {
String formatted = LogFormatUtils.formatValue(result, !traceOn);
return "Resume with async result [" + formatted + "]";
});
invocableMethod = invocableMethod.wrapConcurrentResult(result);
}

// 执行调用处理器
invocableMethod.invokeAndHandle(webRequest, mavContainer);
if (asyncManager.isConcurrentHandlingStarted()) {
return null;
}

return getModelAndView(mavContainer, modelFactory, webRequest);
}
finally {
webRequest.requestCompleted();
}
}

ServletInvocableHandlerMethod处理请求调用

拿到调用的 ServletWebRequest 以及 ModelAndViewContainer 开始处理请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {

// 转换请求参数到目标方法,调用方法并拿到返回值处理
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
// 处理相应状态
setResponseStatus(webRequest);

if (returnValue == null) {
if (isRequestNotModified(webRequest) getResponseStatus() != null mavContainer.isRequestHandled()) {
disableContentCachingIfNecessary(webRequest);
mavContainer.setRequestHandled(true);
return;
}
}
else if (StringUtils.hasText(getResponseStatusReason())) {
mavContainer.setRequestHandled(true);
return;
}

mavContainer.setRequestHandled(false);
Assert.state(this.returnValueHandlers != null, "No return value handlers");
try {
// 返回值处理器将控制器的返回值处理成客户端期待的协议,比如JSON
this.returnValueHandlers.handleReturnValue(
returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
}
catch (Exception ex) {
if (logger.isTraceEnabled()) {
logger.trace(formatErrorForReturnValue(returnValue), ex);
}
throw ex;
}
}

转换请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
@Nullable
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {

Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
if (logger.isTraceEnabled()) {
logger.trace("Arguments: " + Arrays.toString(args));
}
return doInvoke(args);
}
// 获取方法的参数签名
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {

// 当前没有请求参数,直接返回空的参数值
MethodParameter[] parameters = getMethodParameters();
if (ObjectUtils.isEmpty(parameters)) {
return EMPTY_ARGS;
}

Object[] args = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) {
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
args[i] = findProvidedArgument(parameter, providedArgs);
if (args[i] != null) {
continue;
}
if (!this.resolvers.supportsParameter(parameter)) {
throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
}
try {
// 调用处理器翻译请求参数到JavaBean
// 里层就是轮询所有翻译器,返回能够执行的进行调用
args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
}
catch (Exception ex) {
// Leave stack trace for later, exception may actually be resolved and handled...
if (logger.isDebugEnabled()) {
String exMsg = ex.getMessage();
if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
logger.debug(formatArgumentError(parameter, exMsg));
}
}
throw ex;
}
}
return args;
}

通过反射调用控制器方法执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Nullable
protected Object doInvoke(Object... args) throws Exception {
ReflectionUtils.makeAccessible(getBridgedMethod());
try {
// 传递当前对象以及参数执行,拿到返回值进行返回
return getBridgedMethod().invoke(getBean(), args);
}
catch (IllegalArgumentException ex) {
assertTargetBean(getBridgedMethod(), getBean(), args);
String text = (ex.getMessage() != null ? ex.getMessage() : "Illegal argument");
throw new IllegalStateException(formatInvokeError(text, args), ex);
}
catch (InvocationTargetException ex) {
// Unwrap for HandlerExceptionResolvers ...
Throwable targetException = ex.getTargetException();
if (targetException instanceof RuntimeException) {
throw (RuntimeException) targetException;
}
else if (targetException instanceof Error) {
throw (Error) targetException;
}
else if (targetException instanceof Exception) {
throw (Exception) targetException;
}
else {
throw new IllegalStateException(formatInvokeError("Invocation failure", args), targetException);
}
}
}

HandlerMethodReturnValueHandlerComposite处理返回值写出

循环所有消息处理器,拿到第一个支持的 HandlerMethodReturnValueHandler 调用处理器将结果写出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {

// 拿到RequestResponseBodyMethodProcessor处理器进行结果处理
HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
if (handler == null) {
throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
}
handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
}

@Nullable
private HandlerMethodReturnValueHandler selectHandler(@Nullable Object value, MethodParameter returnType) {
boolean isAsyncValue = isAsyncReturnValue(value, returnType);
for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
if (isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler)) {
continue;
}
if (handler.supportsReturnType(returnType)) {
return handler;
}
}
return null;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// AbstractMessageConverterMethodProcessor.java
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {

mavContainer.setRequestHandled(true);
// 重新封装Request和Response
ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);

// 父级方法,执行消息转换
writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}

获取返回值,根据 HTTPHeaders 进行匹配转换器,转换写入 Response 流中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType,
ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {

Object body;
Class<?> valueType;
Type targetType;

// 判断返回值
if (value instanceof CharSequence) {
body = value.toString();
valueType = String.class;
targetType = String.class;
}
else {
body = value;
valueType = getReturnValueType(body, returnType);
targetType = GenericTypeResolver.resolveType(getGenericType(returnType), returnType.getContainingClass());
}

// 是不是写出流,用于文件下载
if (isResourceType(value, returnType)) {
outputMessage.getHeaders().set(HttpHeaders.ACCEPT_RANGES, "bytes");
if (value != null && inputMessage.getHeaders().getFirst(HttpHeaders.RANGE) != null &&
outputMessage.getServletResponse().getStatus() == 200) {
Resource resource = (Resource) value;
try {
List<HttpRange> httpRanges = inputMessage.getHeaders().getRange();
outputMessage.getServletResponse().setStatus(HttpStatus.PARTIAL_CONTENT.value());
body = HttpRange.toResourceRegions(httpRanges, resource);
valueType = body.getClass();
targetType = RESOURCE_REGION_LIST_TYPE;
}
catch (IllegalArgumentException ex) {
outputMessage.getHeaders().set(HttpHeaders.CONTENT_RANGE, "bytes */" + resource.contentLength());
outputMessage.getServletResponse().setStatus(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE.value());
}
}
}

// 根据Content-Type或Accept进行匹配转换器
MediaType selectedMediaType = null;
MediaType contentType = outputMessage.getHeaders().getContentType();
boolean isContentTypePreset = contentType != null && contentType.isConcrete();
if (isContentTypePreset) {
if (logger.isDebugEnabled()) {
logger.debug("Found 'Content-Type:" + contentType + "' in response");
}
selectedMediaType = contentType;
}
else {
HttpServletRequest request = inputMessage.getServletRequest();
// 客户端请求的MediaType
List<MediaType> acceptableTypes = getAcceptableMediaTypes(request);
// 项目允许的MediaType
List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);

if (body != null && producibleTypes.isEmpty()) {
throw new HttpMessageNotWritableException(
"No converter found for return value of type: " + valueType);
}
// 轮询获取可以使用的MediaType
List<MediaType> mediaTypesToUse = new ArrayList<>();
for (MediaType requestedType : acceptableTypes) {
for (MediaType producibleType : producibleTypes) {
if (requestedType.isCompatibleWith(producibleType)) {
mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));
}
}
}
if (mediaTypesToUse.isEmpty()) {
if (body != null) {
throw new HttpMediaTypeNotAcceptableException(producibleTypes);
}
if (logger.isDebugEnabled()) {
logger.debug("No match for " + acceptableTypes + ", supported: " + producibleTypes);
}
return;
}

MediaType.sortBySpecificityAndQuality(mediaTypesToUse);

// 遍历拿到第一个可以执行的MediaType
for (MediaType mediaType : mediaTypesToUse) {
if (mediaType.isConcrete()) {
selectedMediaType = mediaType;
break;
}
else if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) {
selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
break;
}
}

if (logger.isDebugEnabled()) {
logger.debug("Using '" + selectedMediaType + "', given " +
acceptableTypes + " and supported " + producibleTypes);
}
}

if (selectedMediaType != null) {
selectedMediaType = selectedMediaType.removeQualityValue();
// 轮询HttpMessageConverter拿到可以应用于当前MediaType和返回值的转换器
for (HttpMessageConverter<?> converter : this.messageConverters) {
GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ?
(GenericHttpMessageConverter<?>) converter : null);
// 判断是否可写,此时拿到MappingJackson2HttpMessageConverter
if (genericConverter != null ?
((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) :
converter.canWrite(valueType, selectedMediaType)) {
body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType,
(Class<? extends HttpMessageConverter<?>>) converter.getClass(),
inputMessage, outputMessage);
if (body != null) {
Object theBody = body;
LogFormatUtils.traceDebug(logger, traceOn ->
"Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]");
// 如果是下载文件新增必要的HTTP头信息
addContentDispositionHeader(inputMessage, outputMessage);
if (genericConverter != null) {
// 写出
genericConverter.write(body, targetType, selectedMediaType, outputMessage);
}
else {
((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage);
}
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Nothing to write: null body");
}
}
return;
}
}
}

if (body != null) {
if (isContentTypePreset) {
throw new HttpMessageNotWritableException(
"No converter for [" + valueType + "] with preset Content-Type '" + contentType + "'");
}
throw new HttpMediaTypeNotAcceptableException(this.allSupportedMediaTypes);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Override
public final void write(final T t, @Nullable final Type type, @Nullable MediaType contentType,
HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {

final HttpHeaders headers = outputMessage.getHeaders();
addDefaultHeaders(headers, t, contentType);

// 判断是否支持流信息写出
if (outputMessage instanceof StreamingHttpOutputMessage) {
StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
streamingOutputMessage.setBody(outputStream -> writeInternal(t, type, new HttpOutputMessage() {
@Override
public OutputStream getBody() {
return outputStream;
}
@Override
public HttpHeaders getHeaders() {
return headers;
}
}));
}
else {
// 写出并刷新
writeInternal(t, type, outputMessage);
outputMessage.getBody().flush();
}
}

使用 JSON 流的形式写出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@Override
protected void writeInternal(Object object, @Nullable Type type, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {

MediaType contentType = outputMessage.getHeaders().getContentType();
JsonEncoding encoding = getJsonEncoding(contentType);
JsonGenerator generator = this.objectMapper.getFactory().createGenerator(outputMessage.getBody(), encoding);
try {
writePrefix(generator, object);

Object value = object;
Class<?> serializationView = null;
FilterProvider filters = null;
JavaType javaType = null;

if (object instanceof MappingJacksonValue) {
MappingJacksonValue container = (MappingJacksonValue) object;
value = container.getValue();
serializationView = container.getSerializationView();
filters = container.getFilters();
}
if (type != null && TypeUtils.isAssignable(type, value.getClass())) {
javaType = getJavaType(type, null);
}

ObjectWriter objectWriter = (serializationView != null ?
this.objectMapper.writerWithView(serializationView) : this.objectMapper.writer());
if (filters != null) {
objectWriter = objectWriter.with(filters);
}
if (javaType != null && javaType.isContainerType()) {
objectWriter = objectWriter.forType(javaType);
}
SerializationConfig config = objectWriter.getConfig();
if (contentType != null && contentType.isCompatibleWith(MediaType.TEXT_EVENT_STREAM) &&
config.isEnabled(SerializationFeature.INDENT_OUTPUT)) {
objectWriter = objectWriter.with(this.ssePrettyPrinter);
}
// 写入JSON值
objectWriter.writeValue(generator, value);

writeSuffix(generator, object);
generator.flush();
}
catch (InvalidDefinitionException ex) {
throw new HttpMessageConversionException("Type definition error: " + ex.getType(), ex);
}
catch (JsonProcessingException ex) {
throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getOriginalMessage(), ex);
}
}

doDispatch直接其他善后操作

1
2
3
4
5
6
7
8
9
10
11
12
13
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
// ...
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

if (asyncManager.isConcurrentHandlingStarted()) {
return;
}

applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
// ...
}

后面的工作就是轮询拦截器执行后处理的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Apply postHandle methods of registered interceptors.
*/
void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv)
throws Exception {

HandlerInterceptor[] interceptors = getInterceptors();
if (!ObjectUtils.isEmpty(interceptors)) {
for (int i = interceptors.length - 1; i >= 0; i--) {
HandlerInterceptor interceptor = interceptors[i];
// 调用我们写的拦截器
interceptor.postHandle(request, response, this.handler, mv);
}
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// AbstractHandlerMethodMapping.java
protected void processCandidateBean(String beanName) {
Class<?> beanType = null;
try {
beanType = obtainApplicationContext().getType(beanName);
}
catch (Throwable ex) {
// An unresolvable bean type, probably from a lazy bean - let's ignore it.
if (logger.isTraceEnabled()) {
logger.trace("Could not resolve type for bean '" + beanName + "'", ex);
}
}
// 关键是 isHandler,是处理器就注册
if (beanType != null && isHandler(beanType)) {
detectHandlerMethods(beanName);
}
}

Bean@Controller 或者 @RequestMapping 就判定是一个控制器:

1
2
3
4
5
6
// isHandler: 
@Override
protected boolean isHandler(Class<?> beanType) {
return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class)
AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
}

注册处理器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// AbstractHandlerMethodMapping.java
protected void detectHandlerMethods(Object handler) {
Class<?> handlerType = (handler instanceof String ?
obtainApplicationContext().getType((String) handler) : handler.getClass());

if (handlerType != null) {
Class<?> userType = ClassUtils.getUserClass(handlerType);
// 获取处理方法和对应映射的url
// 处理映射url的时候还关系到配置的其他Path的混合等等
Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
(MethodIntrospector.MetadataLookup<T>) method -> {
try {
return getMappingForMethod(method, userType);
}
catch (Throwable ex) {
throw new IllegalStateException("Invalid mapping on handler class [" +
userType.getName() + "]: " + method, ex);
}
});
if (logger.isTraceEnabled()) {
logger.trace(formatMappings(userType, methods));
}
// 循环处理器和url,注册到当前容器
methods.forEach((method, mapping) -> {
// 获取代理方法,如果加上@Validate就会用这里,但是目前没有,先不看
Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
registerHandlerMethod(handler, invocableMethod, mapping);
});
}
}

接下来我们看注册控制器,Spring 又定义了一个 Mapping 容器,用于注册 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// RequestMappingHandlerMapping.java:
@Override
protected void registerHandlerMethod(Object handler, Method method, RequestMappingInfo mapping) {
super.registerHandlerMethod(handler, method, mapping);
// 然后根据情况,消费者的协议如果定义就更新条件
updateConsumesCondition(mapping, method);
}
// AbstractHandlerMethodMapping.java
protected void registerHandlerMethod(Object handler, Method method, T mapping) {
// 向下面定义的控制器容器注册处理器.
this.mappingRegistry.register(mapping, handler, method);
}
/**
* 一个持有所有控制器的容器
*/
class MappingRegistry {

private final Map<T, MappingRegistration<T>> registry = new HashMap<>();

private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap<>();

// 通过url查找对应的处理器
private final MultiValueMap<String, T> urlLookup = new LinkedMultiValueMap<>();

private final Map<String, List<HandlerMethod>> nameLookup = new ConcurrentHashMap<>();

private final Map<HandlerMethod, CorsConfiguration> corsLookup = new ConcurrentHashMap<>();

private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

//..
}

DispatcherServlet装配(二)

好了,在上面的装配中插入了 RequestMappingHandlerMapping 的装配,现在回来继续看两个方法:initHandlerMappingsinitHandlerAdapters

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
private void initHandlerMappings(ApplicationContext context) {
this.handlerMappings = null;

if (this.detectAllHandlerMappings) {
// Find all HandlerMappings in the ApplicationContext, including ancestor contexts.
Map<String, HandlerMapping> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
if (!matchingBeans.isEmpty()) {
// 把 handlerMappings 缓存在 DispatcherServlet
this.handlerMappings = new ArrayList<>(matchingBeans.values());
// We keep HandlerMappings in sorted order.
AnnotationAwareOrderComparator.sort(this.handlerMappings);
}
}
else {
try {
HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
this.handlerMappings = Collections.singletonList(hm);
}
catch (NoSuchBeanDefinitionException ex) {
// Ignore, we'll add a default HandlerMapping later.
}
}

// Ensure we have at least one HandlerMapping, by registering
// a default HandlerMapping if no other mappings are found.
if (this.handlerMappings == null) {
this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
if (logger.isTraceEnabled()) {
logger.trace("No HandlerMappings declared for servlet '" + getServletName() +
"': using default strategies from DispatcherServlet.properties");
}
}
}

initHandlerAdapters 也差不多代码就不贴上来了。

处理HTTP请求准备

HTTPServlet 处理请求是在 service 方法中处理的,所以现在我们需要先进入核心的这个方法中去看: FrameworkServlet 即这个 HTTPServlet 的第一个实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* Delegate GET requests to processRequest/doService.
* <p>Will also be invoked by HttpServlet's default implementation of {@code doHead},
* with a {@code NoBodyResponse} that just captures the content length.
* @see #doService
* @see #doHead
*/
@Override
protected final void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {

processRequest(request, response);
}

/**
* Delegate POST requests to {@link #processRequest}.
* @see #doService
*/
@Override
protected final void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {

processRequest(request, response);
}

/**
* Delegate PUT requests to {@link #processRequest}.
* @see #doService
*/
@Override
protected final void doPut(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {

processRequest(request, response);
}

/**
* Delegate DELETE requests to {@link #processRequest}.
* @see #doService
*/
@Override
protected final void doDelete(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {

processRequest(request, response);
}

那么其实在 FrameworkServlet 中,每一个 doXXX 都是交给 processRequest(request, response); 来做处理,所以现在我们主要看这个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {

long startTime = System.currentTimeMillis();
Throwable failureCause = null;

LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
LocaleContext localeContext = buildLocaleContext(request);

RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);

// 当我们控制器方法是一个DeferredResult的时候会用到异步Web管理器,不过目前我们没有先跳过这部分
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());

// RequestContextHolder初始化,将当前请求的参数设置到里面,这样我们就可以在任何地方拿到请求信息
initContextHolders(request, localeContext, requestAttributes);

try {
// 在子类实现的部分
doService(request, response);
}
catch (ServletException IOException ex) {
failureCause = ex;
throw ex;
}
catch (Throwable ex) {
failureCause = ex;
throw new NestedServletException("Request processing failed", ex);
}

finally {
// 清理RequestContextHolder
resetContextHolders(request, previousLocaleContext, previousAttributes);
if (requestAttributes != null) {
requestAttributes.requestCompleted();
}
logResult(request, response, failureCause, asyncManager);
// 发布请求事件,这里可以用来做操作记录吧,无论成功还是失败都能够拿到
publishRequestHandledEvent(request, response, startTime, failureCause);
}
}

查找控制器进行处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
logRequest(request);

// 保存请求快照,下面恢复可以使用
Map<String, Object> attributesSnapshot = null;
if (WebUtils.isIncludeRequest(request)) {
attributesSnapshot = new HashMap<>();
Enumeration<?> attrNames = request.getAttributeNames();
while (attrNames.hasMoreElements()) {
String attrName = (String) attrNames.nextElement();
if (this.cleanupAfterInclude attrName.startsWith(DEFAULT_STRATEGIES_PREFIX)) {
attributesSnapshot.put(attrName, request.getAttribute(attrName));
}
}
}

// 一些框架的设置,设置到Request中以供用户控制器使用
request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());

// 保存flashMap
if (this.flashMapManager != null) {
FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
if (inputFlashMap != null) {
request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
}
request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
}

try {
// 开始查找执行的控制器
doDispatch(request, response);
}
finally {
if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
// Restore the original attribute snapshot, in case of an include.
if (attributesSnapshot != null) {
restoreAttributesAfterInclude(request, attributesSnapshot);
}
}
}
}

doDispatch 就是查找控制器,并且整合拦截器调用的过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;

WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

try {
ModelAndView mv = null;
Exception dispatchException = null;

try {
// 判断请求是否是上传的请求
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);

// 查找当前请求的处理器
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}

// 查找适配器,用于整合一些拦截器等等
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

// 处理 last-modified 请求头
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}

if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}

// 调用用户的控制器以及整合的拦截器处理请求,这时候请求会被写到Response中
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

if (asyncManager.isConcurrentHandlingStarted()) {
return;
}

// 处理视图,但是目前是JSON,是个空处理
applyDefaultViewName(processedRequest, mv);
// 执行其他的拦截器周期
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}

目前的拦截器链有四个:

  1. RequestMappingHandlerAdapter
  2. HandlerFunctionAdapter
  3. HttpRequestHandlerAdapter
  4. SimpleControllerHandlerAdapter

在查找的过程中,返回第一个匹配的适配器:

1
2
3
4
5
6
7
8
9
10
11
protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
if (this.handlerAdapters != null) {
for (HandlerAdapter adapter : this.handlerAdapters) {
if (adapter.supports(handler)) {
return adapter;
}
}
}
throw new ServletException("No adapter for handler [" + handler +
"]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
}

那么我们现在拿到的实例就是 RequestMappingHandlerAdapter: 然后看看调用 mv = ha.handle(processedRequest, response, mappedHandler.getHandler()) 做什么什么事情:

1
2
3
4
5
6
7
@Override
@Nullable
public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {

return handleInternal(request, response, (HandlerMethod) handler);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Override
protected ModelAndView handleInternal(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {

ModelAndView mav;
checkRequest(request);

// 并发操作,查询是否需要加锁处理
if (this.synchronizeOnSession) {
HttpSession session = request.getSession(false);
if (session != null) {
Object mutex = WebUtils.getSessionMutex(session);
synchronized (mutex) {
mav = invokeHandlerMethod(request, response, handlerMethod);
}
}
else {
// No HttpSession available -> no mutex necessary
mav = invokeHandlerMethod(request, response, handlerMethod);
}
}
else {
// 没有Session需要同步,直接走这里
mav = invokeHandlerMethod(request, response, handlerMethod);
}

if (!response.containsHeader(HEADER_CACHE_CONTROL)) {
if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers);
}
else {
prepareResponse(response);
}
}

return mav;
}

构建请求处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {

// 将request和response封装成ServletWebRequest对象
ServletWebRequest webRequest = new ServletWebRequest(request, response);
try {
// 获取WebDataBinderFactory工厂处理表单多选项的数据绑定问题
// 如CheckBox需要绑定到对象的集合中
WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
// 处理参数为Model方法的工厂
ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);

// 设置参数处理器,包括{PATH}、RequestParam等的处理,以及返回值转换处理器
ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
if (this.argumentResolvers != null) {
invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}
if (this.returnValueHandlers != null) {
invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
}
invocableMethod.setDataBinderFactory(binderFactory);
invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);

// 初始化执行容器 ModelAndViewContainer
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
modelFactory.initModel(webRequest, mavContainer, invocableMethod);
mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);

AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);
asyncWebRequest.setTimeout(this.asyncRequestTimeout);

WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.setTaskExecutor(this.taskExecutor);
asyncManager.setAsyncWebRequest(asyncWebRequest);
asyncManager.registerCallableInterceptors(this.callableInterceptors);
asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors);

// 异步处理器的处理
if (asyncManager.hasConcurrentResult()) {
Object result = asyncManager.getConcurrentResult();
mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0];
asyncManager.clearConcurrentResult();
LogFormatUtils.traceDebug(logger, traceOn -> {
String formatted = LogFormatUtils.formatValue(result, !traceOn);
return "Resume with async result [" + formatted + "]";
});
invocableMethod = invocableMethod.wrapConcurrentResult(result);
}

// 执行调用处理器
invocableMethod.invokeAndHandle(webRequest, mavContainer);
if (asyncManager.isConcurrentHandlingStarted()) {
return null;
}

return getModelAndView(mavContainer, modelFactory, webRequest);
}
finally {
webRequest.requestCompleted();
}
}

简述

由于现在基本使用的都是 SpringBoot 项目来启动,所以我就直接建立在 SpringBoot 基础下看 WEB 容器了。 如果有一些项目开发经验就知道,SpringWEB 模块又名 SpringMVC,提供了视图数据整合以及可以直接返回 JSON 数据。前几年 JSP 当道的时候,各种 ViewResolver 层出不穷,据我所知的就有 FreeMarker JSP Beetl 等等。不过最近几年前端发生爆炸性变化,前后端能分离的都分离了,需要 SEO 的也很少使用 Java 来做了。所以现在 Java 后端项目的主要责职是提供业务所需要的交互数据给前端,不仅仅使用 JSON,还可以整合其他的传输协议,比如 Hessian XML Protobuf 等等,只要够折腾,什么格式都可以返回,不想折腾就使用 JSON 就可以了。 数据转换模块也会被 Spring 独立出来这个自然不需要再猜测,所以我们也可以为了减少传输使用的宽带,去定义第几个字节是什么东西。 那我们现在可以直接进入 SpringMVC 的源码来看看了。

初始化Web环境

JavaWEB 中,如果需要提供网络服务,一般是实现 Servlet 对象,像 Tomcat Resign 等网络容器都提供了自己的实现,我们就简单的用常用的 Tomcat 来说,Tomcat 会拿到我们项目中所有的 HTTPServlet 然后根据不同的 HTTP Method HTTP URL 来调用不同的 Servlet 方法来完成请求。 那 SpringMVC 是在 Servlet 级别的,所以这个容器必定会存在,那就是 DispatcherServlet。简单的理解 DispatcherServlet 就是一个在项目启动时读取了所有请求处理器,然后有请求进来根据不同的定义来调用不同的请求处理器的一个 Servlet 容器。 把断点打在 DispatcherServlet 的无参构造器上,找到初始化入口,是在 refreshContext(context) 中进行的,那我们就从这里开始:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
configureHeadlessProperty();
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
// 我们之前说过,这里会根据依赖的 jar 环境来创建不同的 Context
// 所以现在创建的是 AnnotationConfigServletWebServerApplicationContext 容器
context = createApplicationContext();
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
// 刷新容器
refreshContext(context);
afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
listeners.started(context);
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}

try {
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
return context;
}

一系列之前说过的步骤以后,来到这里:

1
2
3
4
protected void refresh(ApplicationContext applicationContext) {
Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext);
((AbstractApplicationContext) applicationContext).refresh();
}

之前说过,调用 refresh 是个模板方法,AbstractApplicationContextrefresh 是个模板方法,会调用子类实现的 onRefresh。因为当前是个 WEB 项目,所以实现类 AnnotationConfigServletWebServerApplicationContextonRefresh 提供了实现:

1
2
3
4
5
6
7
8
9
10
@Override
protected void onRefresh() {
super.onRefresh();
try {
createWebServer();
}
catch (Throwable ex) {
throw new ApplicationContextException("Unable to start web server", ex);
}
}

第一件事请 super.onRefresh() 是调用父级 GenericWebApplicationContext 的函数,不过这段函数现在看来应该没什么多大的用处了:

1
2
3
4
5
6
7
/**
* 初始化界面主题容器
*/
@Override
protected void onRefresh() {
this.themeSource = UiApplicationContextUtils.initThemeSource(this);
}

是一个初始化界面主题的函数,一开始我还怀疑这个主题是不是什么主题,然而进入 UiApplicationContextUtils.initThemeSource 看的时候,还真的就是 HTML 主题,里面定义了从哪里读取 CSS 呀这些鬼东西,跳过不看。 重点来看看 createWebServer() 做了哪些事情:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private void createWebServer() {
WebServer webServer = this.webServer;
ServletContext servletContext = getServletContext();
if (webServer == null && servletContext == null) {
// 工厂模式
ServletWebServerFactory factory = getWebServerFactory();
// 创建webServer
this.webServer = factory.getWebServer(getSelfInitializer());
}
else if (servletContext != null) {
try {
getSelfInitializer().onStartup(servletContext);
}
catch (ServletException ex) {
throw new ApplicationContextException("Cannot initialize servlet context", ex);
}
}
// 初始化配置属性
initPropertySources();
}

这时候获取了一个 ServletWebServerFactoryBeanDefinition,然后使用这个 Factory 来创建我们目前的 Web 环境。

1
2
3
4
5
6
7
8
9
10
11
12
13
protected ServletWebServerFactory getWebServerFactory() {
// Use bean names so that we don't consider the hierarchy
String[] beanNames = getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class);
if (beanNames.length == 0) {
throw new ApplicationContextException("Unable to start ServletWebServerApplicationContext due to missing "
+ "ServletWebServerFactory bean.");
}
if (beanNames.length > 1) {
throw new ApplicationContextException("Unable to start ServletWebServerApplicationContext due to multiple "
+ "ServletWebServerFactory beans : " + StringUtils.arrayToCommaDelimitedString(beanNames));
}
return getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class);
}

内置Tomcat注入

然而这个ServletWebServerFactory 什么时候偷偷被放进去的,我们现在快速过一遍:

  1. SpringBoot 启动的时候,在构造器调用了元信息读取器,读取了 META-INF/spring.factories 中配置的工厂,后初始化的时候会用到这些配置的类;
  2. SpringBoot 中上面的配置文件配置了这个工厂:ServletWebServerFactoryAutoConfiguration
  3. 看这个工厂有什么信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Configuration(proxyBeanMethods = false)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@ConditionalOnClass(ServletRequest.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(ServerProperties.class)
@Import({ ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,
ServletWebServerFactoryConfiguration.EmbeddedTomcat.class,
ServletWebServerFactoryConfiguration.EmbeddedJetty.class,
ServletWebServerFactoryConfiguration.EmbeddedUndertow.class })
public class ServletWebServerFactoryAutoConfiguration {

// 自定义容器的初始化:
@Bean
public ServletWebServerFactoryCustomizer servletWebServerFactoryCustomizer(ServerProperties serverProperties) {
return new ServletWebServerFactoryCustomizer(serverProperties);
}

@Bean
@ConditionalOnClass(name = "org.apache.catalina.startup.Tomcat")
public TomcatServletWebServerFactoryCustomizer tomcatServletWebServerFactoryCustomizer(
ServerProperties serverProperties) {
return new TomcatServletWebServerFactoryCustomizer(serverProperties);
}

// 其他容器初始化...
}

然后我们看到上面导入那里导入了内置 Tomcat

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Configuration(proxyBeanMethods = false)
class ServletWebServerFactoryConfiguration {

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class })
@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
public static class EmbeddedTomcat {

@Bean
public TomcatServletWebServerFactory tomcatServletWebServerFactory(
ObjectProvider<TomcatConnectorCustomizer> connectorCustomizers,
ObjectProvider<TomcatContextCustomizer> contextCustomizers,
ObjectProvider<TomcatProtocolHandlerCustomizer<?>> protocolHandlerCustomizers) {
TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
factory.getTomcatConnectorCustomizers()
.addAll(connectorCustomizers.orderedStream().collect(Collectors.toList()));
factory.getTomcatContextCustomizers()
.addAll(contextCustomizers.orderedStream().collect(Collectors.toList()));
factory.getTomcatProtocolHandlerCustomizers()
.addAll(protocolHandlerCustomizers.orderedStream().collect(Collectors.toList()));
return factory;
}

}
//...
}

所以这个 ServletWebServerFactory 就在读取配置的时候被自动导入到 BeanDefinitionMaps 中去。

DispatcherServlet构建

怎么会涉及这一块的初始化呢,是因为上面 ServletWebServerFactoryConfiguration 初始化完成以后,触发的 ServletWebServerFactoryConfiguration 初始化,先来看看 ServletWebServerFactoryConfiguration 的注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass(DispatcherServlet.class)
@AutoConfigureAfter(ServletWebServerFactoryAutoConfiguration.class)
public class DispatcherServletAutoConfiguration {

/*
* The bean name for a DispatcherServlet that will be mapped to the root URL "/"
*/
public static final String DEFAULT_DISPATCHER_SERVLET_BEAN_NAME = "dispatcherServlet";

/*
* The bean name for a ServletRegistrationBean for the DispatcherServlet "/"
*/
public static final String DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME = "dispatcherServletRegistration";

@Configuration(proxyBeanMethods = false)
@Conditional(DefaultDispatcherServletCondition.class)
@ConditionalOnClass(ServletRegistration.class)
@EnableConfigurationProperties({ HttpProperties.class, WebMvcProperties.class })
protected static class DispatcherServletConfiguration {

@Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public DispatcherServlet dispatcherServlet(HttpProperties httpProperties, WebMvcProperties webMvcProperties) {
DispatcherServlet dispatcherServlet = new DispatcherServlet();
dispatcherServlet.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest());
dispatcherServlet.setDispatchTraceRequest(webMvcProperties.isDispatchTraceRequest());
dispatcherServlet.setThrowExceptionIfNoHandlerFound(webMvcProperties.isThrowExceptionIfNoHandlerFound());
dispatcherServlet.setPublishEvents(webMvcProperties.isPublishRequestHandledEvents());
dispatcherServlet.setEnableLoggingRequestDetails(httpProperties.isLogRequestDetails());
return dispatcherServlet;
}

@Bean
@ConditionalOnBean(MultipartResolver.class)
@ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)
public MultipartResolver multipartResolver(MultipartResolver resolver) {
// Detect if the user has created a MultipartResolver but named it incorrectly
return resolver;
}

}
// 其他暂时跳过的配置
}

这个初始化就很有灵性了,跟我们平常写的配置类大致一致。那关于 DispatcherServlet 的配置,我们均可以通过配置 WebMvcProperties 来让他注入到当前的 DispatcherServlet。 这就完了😱?映射呢映射呢? 好吧,差点忘记此时 HTTPServlet 还没有初始化。

DispatcherServlet初始化(一)

默认的 HTTPServlet 在启动的时候只有装载,并没有初始化,初始化发生在第一次请求的时候。 我们来看看这个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public abstract class HttpServletBean extends HttpServlet implements EnvironmentCapable, EnvironmentAware {
@Override
public final void init() throws ServletException {

// 读取配置.
PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
if (!pvs.isEmpty()) {
try {
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
initBeanWrapper(bw);
bw.setPropertyValues(pvs, true);
}
catch (BeansException ex) {
if (logger.isErrorEnabled()) {
logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
}
throw ex;
}
}

// 子类实现初始化.
initServletBean();
}
}

FrameworkServlet 实现上面的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
@Override
protected final void initServletBean() throws ServletException {
getServletContext().log("Initializing Spring " + getClass().getSimpleName() + " '" + getServletName() + "'");
if (logger.isInfoEnabled()) {
logger.info("Initializing Servlet '" + getServletName() + "'");
}
long startTime = System.currentTimeMillis();

try {
// 初始化Web应用的上下文
this.webApplicationContext = initWebApplicationContext();
// 初始化
initFrameworkServlet();
}
catch (ServletException RuntimeException ex) {
logger.error("Context initialization failed", ex);
throw ex;
}

if (logger.isDebugEnabled()) {
String value = this.enableLoggingRequestDetails ?
"shown which may lead to unsafe logging of potentially sensitive data" :
"masked to prevent unsafe logging of potentially sensitive data";
logger.debug("enableLoggingRequestDetails='" + this.enableLoggingRequestDetails +
"': request parameters and headers will be " + value);
}

if (logger.isInfoEnabled()) {
logger.info("Completed initialization in " + (System.currentTimeMillis() - startTime) + " ms");
}
}
protected WebApplicationContext initWebApplicationContext() {
WebApplicationContext rootContext =
WebApplicationContextUtils.getWebApplicationContext(getServletContext());
WebApplicationContext wac = null;

if (this.webApplicationContext != null) {
// 当前rootContext已经存在了
wac = this.webApplicationContext;
if (wac instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
// 已经激活,这里并不会进来
if (!cwac.isActive()) {
// The context has not yet been refreshed -> provide services such as
// setting the parent context, setting the application context id, etc
if (cwac.getParent() == null) {
// The context instance was injected without an explicit parent -> set
// the root application context (if any; may be null) as the parent
cwac.setParent(rootContext);
}
configureAndRefreshWebApplicationContext(cwac);
}
}
}
if (wac == null) {
// No context instance was injected at construction time -> see if one
// has been registered in the servlet context. If one exists, it is assumed
// that the parent context (if any) has already been set and that the
// user has performed any initialization such as setting the context id
wac = findWebApplicationContext();
}
if (wac == null) {
// No context instance is defined for this servlet -> create a local one
wac = createWebApplicationContext(rootContext);
}

if (!this.refreshEventReceived) {
// 刷新上下文
synchronized (this.onRefreshMonitor) {
onRefresh(wac);
}
}

if (this.publishContext) {
// Publish the context as a servlet context attribute.
String attrName = getServletContextAttributeName();
getServletContext().setAttribute(attrName, wac);
}

return wac;
}

刷新上下文在上面那么多个容器中已经很有感觉了,就是刷新配置的 Bean 调用对应的构造器 AOP 注入啊啊巴拉巴拉。 所以,onRefresh 是整个 Web 容器初始化的关键:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
DispatcherServlet.java:
@Override
protected void onRefresh(ApplicationContext context) {
initStrategies(context);
}
// 初始化DispatcherServlet所有的内容
protected void initStrategies(ApplicationContext context) {
// 初始化多媒体处理器,处理上传的请求
initMultipartResolver(context);
// i18n初始化
initLocaleResolver(context);
// 项目主题
initThemeResolver(context);
// 请求处理器,如果我们写Controller应该是RequestMappingHandlerMapping处理
initHandlerMappings(context);
// 初始化适配器,会在调用的时候寻找合适的控制器和拦截器整合一起调用
initHandlerAdapters(context);
// 异常处理器
initHandlerExceptionResolvers(context);
// 视图名字处理器,通过控制器返回的字符串查找到对应的视图
initRequestToViewNameTranslator(context);
// 视图处理器,处理返回值与界面的整合
initViewResolvers(context);
// 初始化FlashMap处理器,FlashMap是一个保存数据的地方,
// 在重定向的时候会把参数临时存储在session中
// 跳转完成即清理上一个url携带的数据
initFlashMapManager(context);
}

我大概只对 initHandlerMappingsinitHandlerAdapters 感兴趣。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
private void initHandlerMappings(ApplicationContext context) {
this.handlerMappings = null;

if (this.detectAllHandlerMappings) {
// 从BF中获取所有的HandlerMapping实例进行重组排序
Map<String, HandlerMapping> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
if (!matchingBeans.isEmpty()) {
this.handlerMappings = new ArrayList<>(matchingBeans.values());
// We keep HandlerMappings in sorted order.
AnnotationAwareOrderComparator.sort(this.handlerMappings);
}
}
else {
try {
HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
this.handlerMappings = Collections.singletonList(hm);
}
catch (NoSuchBeanDefinitionException ex) {
// Ignore, we'll add a default HandlerMapping later.
}
}

if (this.handlerMappings == null) {
this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
if (logger.isTraceEnabled()) {
logger.trace("No HandlerMappings declared for servlet '" + getServletName() +
"': using default strategies from DispatcherServlet.properties");
}
}
}

那么这些 handlerMappings 是什么时候被装配的,通过 WebMvcAutoConfiguration 这个配置类自动装配的,并且装配发生在 DispatcherServletAutoConfiguration 之后。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {

// 重写父类createRequestMappingHandlerMapping,带有@Bean注解
@Override
protected RequestMappingHandlerMapping createRequestMappingHandlerMapping() {
if (this.mvcRegistrations != null && this.mvcRegistrations.getRequestMappingHandlerMapping() != null) {
return this.mvcRegistrations.getRequestMappingHandlerMapping();
}
return super.createRequestMappingHandlerMapping();
}

}

RequestMappingHandlerMapping装配

上面我们看到,RequestMappingHandler 是通过自动装配的形式放入 BeanFactory 的,那至于我们编写的 @Controller 以及 @RestController 是怎么被读取的,就是通过装配时,调用 InitializingBean 接口的 afterPropertiesSet 方法进行装配的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// RequestMappingHandlerMapping.java
@Override
public void afterPropertiesSet() {
this.config = new RequestMappingInfo.BuilderConfiguration();
this.config.setUrlPathHelper(getUrlPathHelper());
this.config.setPathMatcher(getPathMatcher());
this.config.setSuffixPatternMatch(this.useSuffixPatternMatch);
this.config.setTrailingSlashMatch(this.useTrailingSlashMatch);
this.config.setRegisteredSuffixPatternMatch(this.useRegisteredSuffixPatternMatch);
this.config.setContentNegotiationManager(getContentNegotiationManager());

super.afterPropertiesSet();
}
// AbstractHandlerMethodMapping.java
@Override
public void afterPropertiesSet() {
initHandlerMethods();
}
protected void initHandlerMethods() {
for (String beanName : getCandidateBeanNames()) {
if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
processCandidateBean(beanName);
}
}
handlerMethodsInitialized(getHandlerMethods());
}

拿到 BeanFactory 中所有的 Bean 一顿循环😶:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// AbstractHandlerMethodMapping.java
protected void processCandidateBean(String beanName) {
Class<?> beanType = null;
try {
beanType = obtainApplicationContext().getType(beanName);
}
catch (Throwable ex) {
// An unresolvable bean type, probably from a lazy bean - let's ignore it.
if (logger.isTraceEnabled()) {
logger.trace("Could not resolve type for bean '" + beanName + "'", ex);
}
}
// 关键是 isHandler,是处理器就注册
if (beanType != null && isHandler(beanType)) {
detectHandlerMethods(beanName);
}
}

Bean@Controller 或者 @RequestMapping 就判定是一个控制器:

1
2
3
4
5
6
// isHandler: 
@Override
protected boolean isHandler(Class<?> beanType) {
return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class)
AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
}

注册处理器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// AbstractHandlerMethodMapping.java
protected void detectHandlerMethods(Object handler) {
Class<?> handlerType = (handler instanceof String ?
obtainApplicationContext().getType((String) handler) : handler.getClass());

if (handlerType != null) {
Class<?> userType = ClassUtils.getUserClass(handlerType);
// 获取处理方法和对应映射的url
// 处理映射url的时候还关系到配置的其他Path的混合等等
Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
(MethodIntrospector.MetadataLookup<T>) method -> {
try {
return getMappingForMethod(method, userType);
}
catch (Throwable ex) {
throw new IllegalStateException("Invalid mapping on handler class [" +
userType.getName() + "]: " + method, ex);
}
});
if (logger.isTraceEnabled()) {
logger.trace(formatMappings(userType, methods));
}
// 循环处理器和url,注册到当前容器
methods.forEach((method, mapping) -> {
// 获取代理方法,如果加上@Validate就会用这里,但是目前没有,先不看
Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
registerHandlerMethod(handler, invocableMethod, mapping);
});
}
}

接下来我们看注册控制器,Spring 又定义了一个 Mapping 容器,用于注册 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// RequestMappingHandlerMapping.java:
@Override
protected void registerHandlerMethod(Object handler, Method method, RequestMappingInfo mapping) {
super.registerHandlerMethod(handler, method, mapping);
// 然后根据情况,消费者的协议如果定义就更新条件
updateConsumesCondition(mapping, method);
}
// AbstractHandlerMethodMapping.java
protected void registerHandlerMethod(Object handler, Method method, T mapping) {
// 向下面定义的控制器容器注册处理器.
this.mappingRegistry.register(mapping, handler, method);
}
/**
* 一个持有所有控制器的容器
*/
class MappingRegistry {

private final Map<T, MappingRegistration<T>> registry = new HashMap<>();

private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap<>();

// 通过url查找对应的处理器
private final MultiValueMap<String, T> urlLookup = new LinkedMultiValueMap<>();

private final Map<String, List<HandlerMethod>> nameLookup = new ConcurrentHashMap<>();

private final Map<HandlerMethod, CorsConfiguration> corsLookup = new ConcurrentHashMap<>();

private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

//..
}

DispatcherServlet装配(二)

好了,在上面的装配中插入了 RequestMappingHandlerMapping 的装配,现在回来继续看两个方法:initHandlerMappingsinitHandlerAdapters

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
private void initHandlerMappings(ApplicationContext context) {
this.handlerMappings = null;

if (this.detectAllHandlerMappings) {
// Find all HandlerMappings in the ApplicationContext, including ancestor contexts.
Map<String, HandlerMapping> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
if (!matchingBeans.isEmpty()) {
// 把 handlerMappings 缓存在 DispatcherServlet
this.handlerMappings = new ArrayList<>(matchingBeans.values());
// We keep HandlerMappings in sorted order.
AnnotationAwareOrderComparator.sort(this.handlerMappings);
}
}
else {
try {
HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
this.handlerMappings = Collections.singletonList(hm);
}
catch (NoSuchBeanDefinitionException ex) {
// Ignore, we'll add a default HandlerMapping later.
}
}

// Ensure we have at least one HandlerMapping, by registering
// a default HandlerMapping if no other mappings are found.
if (this.handlerMappings == null) {
this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
if (logger.isTraceEnabled()) {
logger.trace("No HandlerMappings declared for servlet '" + getServletName() +
"': using default strategies from DispatcherServlet.properties");
}
}
}

initHandlerAdapters 也差不多代码就不贴上来了。

处理HTTP请求准备

HTTPServlet 处理请求是在 service 方法中处理的,所以现在我们需要先进入核心的这个方法中去看: FrameworkServlet 即这个 HTTPServlet 的第一个实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* Delegate GET requests to processRequest/doService.
* <p>Will also be invoked by HttpServlet's default implementation of {@code doHead},
* with a {@code NoBodyResponse} that just captures the content length.
* @see #doService
* @see #doHead
*/
@Override
protected final void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {

processRequest(request, response);
}

/**
* Delegate POST requests to {@link #processRequest}.
* @see #doService
*/
@Override
protected final void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {

processRequest(request, response);
}

/**
* Delegate PUT requests to {@link #processRequest}.
* @see #doService
*/
@Override
protected final void doPut(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {

processRequest(request, response);
}

/**
* Delegate DELETE requests to {@link #processRequest}.
* @see #doService
*/
@Override
protected final void doDelete(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {

processRequest(request, response);
}

那么其实在 FrameworkServlet 中,每一个 doXXX 都是交给 processRequest(request, response); 来做处理,所以现在我们主要看这个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {

long startTime = System.currentTimeMillis();
Throwable failureCause = null;

LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
LocaleContext localeContext = buildLocaleContext(request);

RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);

// 当我们控制器方法是一个DeferredResult的时候会用到异步Web管理器,不过目前我们没有先跳过这部分
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());

// RequestContextHolder初始化,将当前请求的参数设置到里面,这样我们就可以在任何地方拿到请求信息
initContextHolders(request, localeContext, requestAttributes);

try {
// 在子类实现的部分
doService(request, response);
}
catch (ServletException IOException ex) {
failureCause = ex;
throw ex;
}
catch (Throwable ex) {
failureCause = ex;
throw new NestedServletException("Request processing failed", ex);
}

finally {
// 清理RequestContextHolder
resetContextHolders(request, previousLocaleContext, previousAttributes);
if (requestAttributes != null) {
requestAttributes.requestCompleted();
}
logResult(request, response, failureCause, asyncManager);
// 发布请求事件,这里可以用来做操作记录吧,无论成功还是失败都能够拿到
publishRequestHandledEvent(request, response, startTime, failureCause);
}
}

查找控制器进行处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
logRequest(request);

// 保存请求快照,下面恢复可以使用
Map<String, Object> attributesSnapshot = null;
if (WebUtils.isIncludeRequest(request)) {
attributesSnapshot = new HashMap<>();
Enumeration<?> attrNames = request.getAttributeNames();
while (attrNames.hasMoreElements()) {
String attrName = (String) attrNames.nextElement();
if (this.cleanupAfterInclude attrName.startsWith(DEFAULT_STRATEGIES_PREFIX)) {
attributesSnapshot.put(attrName, request.getAttribute(attrName));
}
}
}

// 一些框架的设置,设置到Request中以供用户控制器使用
request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());

// 保存flashMap
if (this.flashMapManager != null) {
FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
if (inputFlashMap != null) {
request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
}
request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
}

try {
// 开始查找执行的控制器
doDispatch(request, response);
}
finally {
if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
// Restore the original attribute snapshot, in case of an include.
if (attributesSnapshot != null) {
restoreAttributesAfterInclude(request, attributesSnapshot);
}
}
}
}

doDispatch 就是查找控制器,并且整合拦截器调用的过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;

WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

try {
ModelAndView mv = null;
Exception dispatchException = null;

try {
// 判断请求是否是上传的请求
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);

// 查找当前请求的处理器
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}

// 查找适配器,用于整合一些拦截器等等
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

// 处理 last-modified 请求头
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}

if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}

// 调用用户的控制器以及整合的拦截器处理请求,这时候请求会被写到Response中
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

if (asyncManager.isConcurrentHandlingStarted()) {
return;
}

// 处理视图,但是目前是JSON,是个空处理
applyDefaultViewName(processedRequest, mv);
// 执行其他的拦截器周期
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}

目前的拦截器链有四个:

  1. RequestMappingHandlerAdapter
  2. HandlerFunctionAdapter
  3. HttpRequestHandlerAdapter
  4. SimpleControllerHandlerAdapter

在查找的过程中,返回第一个匹配的适配器:

1
2
3
4
5
6
7
8
9
10
11
protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
if (this.handlerAdapters != null) {
for (HandlerAdapter adapter : this.handlerAdapters) {
if (adapter.supports(handler)) {
return adapter;
}
}
}
throw new ServletException("No adapter for handler [" + handler +
"]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
}

那么我们现在拿到的实例就是 RequestMappingHandlerAdapter: 然后看看调用 mv = ha.handle(processedRequest, response, mappedHandler.getHandler()) 做什么什么事情:

1
2
3
4
5
6
7
@Override
@Nullable
public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {

return handleInternal(request, response, (HandlerMethod) handler);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Override
protected ModelAndView handleInternal(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {

ModelAndView mav;
checkRequest(request);

// 并发操作,查询是否需要加锁处理
if (this.synchronizeOnSession) {
HttpSession session = request.getSession(false);
if (session != null) {
Object mutex = WebUtils.getSessionMutex(session);
synchronized (mutex) {
mav = invokeHandlerMethod(request, response, handlerMethod);
}
}
else {
// No HttpSession available -> no mutex necessary
mav = invokeHandlerMethod(request, response, handlerMethod);
}
}
else {
// 没有Session需要同步,直接走这里
mav = invokeHandlerMethod(request, response, handlerMethod);
}

if (!response.containsHeader(HEADER_CACHE_CONTROL)) {
if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers);
}
else {
prepareResponse(response);
}
}

return mav;
}

构建请求处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {

// 将request和response封装成ServletWebRequest对象
ServletWebRequest webRequest = new ServletWebRequest(request, response);
try {
// 获取WebDataBinderFactory工厂处理表单多选项的数据绑定问题
// 如CheckBox需要绑定到对象的集合中
WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
// 处理参数为Model方法的工厂
ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);

// 设置参数处理器,包括{PATH}、RequestParam等的处理,以及返回值转换处理器
ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
if (this.argumentResolvers != null) {
invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}
if (this.returnValueHandlers != null) {
invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
}
invocableMethod.setDataBinderFactory(binderFactory);
invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);

// 初始化执行容器 ModelAndViewContainer
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
modelFactory.initModel(webRequest, mavContainer, invocableMethod);
mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);

AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);
asyncWebRequest.setTimeout(this.asyncRequestTimeout);

WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.setTaskExecutor(this.taskExecutor);
asyncManager.setAsyncWebRequest(asyncWebRequest);
asyncManager.registerCallableInterceptors(this.callableInterceptors);
asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors);

// 异步处理器的处理
if (asyncManager.hasConcurrentResult()) {
Object result = asyncManager.getConcurrentResult();
mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0];
asyncManager.clearConcurrentResult();
LogFormatUtils.traceDebug(logger, traceOn -> {
String formatted = LogFormatUtils.formatValue(result, !traceOn);
return "Resume with async result [" + formatted + "]";
});
invocableMethod = invocableMethod.wrapConcurrentResult(result);
}

// 执行调用处理器
invocableMethod.invokeAndHandle(webRequest, mavContainer);
if (asyncManager.isConcurrentHandlingStarted()) {
return null;
}

return getModelAndView(mavContainer, modelFactory, webRequest);
}
finally {
webRequest.requestCompleted();
}
}

ServletInvocableHandlerMethod处理请求调用

拿到调用的 ServletWebRequest 以及 ModelAndViewContainer 开始处理请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {

// 转换请求参数到目标方法,调用方法并拿到返回值处理
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
// 处理相应状态
setResponseStatus(webRequest);

if (returnValue == null) {
if (isRequestNotModified(webRequest) getResponseStatus() != null mavContainer.isRequestHandled()) {
disableContentCachingIfNecessary(webRequest);
mavContainer.setRequestHandled(true);
return;
}
}
else if (StringUtils.hasText(getResponseStatusReason())) {
mavContainer.setRequestHandled(true);
return;
}

mavContainer.setRequestHandled(false);
Assert.state(this.returnValueHandlers != null, "No return value handlers");
try {
// 返回值处理器将控制器的返回值处理成客户端期待的协议,比如JSON
this.returnValueHandlers.handleReturnValue(
returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
}
catch (Exception ex) {
if (logger.isTraceEnabled()) {
logger.trace(formatErrorForReturnValue(returnValue), ex);
}
throw ex;
}
}

转换请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
@Nullable
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {

Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
if (logger.isTraceEnabled()) {
logger.trace("Arguments: " + Arrays.toString(args));
}
return doInvoke(args);
}
// 获取方法的参数签名
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {

// 当前没有请求参数,直接返回空的参数值
MethodParameter[] parameters = getMethodParameters();
if (ObjectUtils.isEmpty(parameters)) {
return EMPTY_ARGS;
}

Object[] args = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) {
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
args[i] = findProvidedArgument(parameter, providedArgs);
if (args[i] != null) {
continue;
}
if (!this.resolvers.supportsParameter(parameter)) {
throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
}
try {
// 调用处理器翻译请求参数到JavaBean
// 里层就是轮询所有翻译器,返回能够执行的进行调用
args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
}
catch (Exception ex) {
// Leave stack trace for later, exception may actually be resolved and handled...
if (logger.isDebugEnabled()) {
String exMsg = ex.getMessage();
if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
logger.debug(formatArgumentError(parameter, exMsg));
}
}
throw ex;
}
}
return args;
}

通过反射调用控制器方法执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Nullable
protected Object doInvoke(Object... args) throws Exception {
ReflectionUtils.makeAccessible(getBridgedMethod());
try {
// 传递当前对象以及参数执行,拿到返回值进行返回
return getBridgedMethod().invoke(getBean(), args);
}
catch (IllegalArgumentException ex) {
assertTargetBean(getBridgedMethod(), getBean(), args);
String text = (ex.getMessage() != null ? ex.getMessage() : "Illegal argument");
throw new IllegalStateException(formatInvokeError(text, args), ex);
}
catch (InvocationTargetException ex) {
// Unwrap for HandlerExceptionResolvers ...
Throwable targetException = ex.getTargetException();
if (targetException instanceof RuntimeException) {
throw (RuntimeException) targetException;
}
else if (targetException instanceof Error) {
throw (Error) targetException;
}
else if (targetException instanceof Exception) {
throw (Exception) targetException;
}
else {
throw new IllegalStateException(formatInvokeError("Invocation failure", args), targetException);
}
}
}

HandlerMethodReturnValueHandlerComposite处理返回值写出

循环所有消息处理器,拿到第一个支持的 HandlerMethodReturnValueHandler 调用处理器将结果写出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {

// 拿到RequestResponseBodyMethodProcessor处理器进行结果处理
HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
if (handler == null) {
throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
}
handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
}

@Nullable
private HandlerMethodReturnValueHandler selectHandler(@Nullable Object value, MethodParameter returnType) {
boolean isAsyncValue = isAsyncReturnValue(value, returnType);
for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
if (isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler)) {
continue;
}
if (handler.supportsReturnType(returnType)) {
return handler;
}
}
return null;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// AbstractMessageConverterMethodProcessor.java
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {

mavContainer.setRequestHandled(true);
// 重新封装Request和Response
ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);

// 父级方法,执行消息转换
writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}

获取返回值,根据 HTTPHeaders 进行匹配转换器,转换写入 Response 流中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType,
ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {

Object body;
Class<?> valueType;
Type targetType;

// 判断返回值
if (value instanceof CharSequence) {
body = value.toString();
valueType = String.class;
targetType = String.class;
}
else {
body = value;
valueType = getReturnValueType(body, returnType);
targetType = GenericTypeResolver.resolveType(getGenericType(returnType), returnType.getContainingClass());
}

// 是不是写出流,用于文件下载
if (isResourceType(value, returnType)) {
outputMessage.getHeaders().set(HttpHeaders.ACCEPT_RANGES, "bytes");
if (value != null && inputMessage.getHeaders().getFirst(HttpHeaders.RANGE) != null &&
outputMessage.getServletResponse().getStatus() == 200) {
Resource resource = (Resource) value;
try {
List<HttpRange> httpRanges = inputMessage.getHeaders().getRange();
outputMessage.getServletResponse().setStatus(HttpStatus.PARTIAL_CONTENT.value());
body = HttpRange.toResourceRegions(httpRanges, resource);
valueType = body.getClass();
targetType = RESOURCE_REGION_LIST_TYPE;
}
catch (IllegalArgumentException ex) {
outputMessage.getHeaders().set(HttpHeaders.CONTENT_RANGE, "bytes */" + resource.contentLength());
outputMessage.getServletResponse().setStatus(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE.value());
}
}
}

// 根据Content-Type或Accept进行匹配转换器
MediaType selectedMediaType = null;
MediaType contentType = outputMessage.getHeaders().getContentType();
boolean isContentTypePreset = contentType != null && contentType.isConcrete();
if (isContentTypePreset) {
if (logger.isDebugEnabled()) {
logger.debug("Found 'Content-Type:" + contentType + "' in response");
}
selectedMediaType = contentType;
}
else {
HttpServletRequest request = inputMessage.getServletRequest();
// 客户端请求的MediaType
List<MediaType> acceptableTypes = getAcceptableMediaTypes(request);
// 项目允许的MediaType
List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);

if (body != null && producibleTypes.isEmpty()) {
throw new HttpMessageNotWritableException(
"No converter found for return value of type: " + valueType);
}
// 轮询获取可以使用的MediaType
List<MediaType> mediaTypesToUse = new ArrayList<>();
for (MediaType requestedType : acceptableTypes) {
for (MediaType producibleType : producibleTypes) {
if (requestedType.isCompatibleWith(producibleType)) {
mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));
}
}
}
if (mediaTypesToUse.isEmpty()) {
if (body != null) {
throw new HttpMediaTypeNotAcceptableException(producibleTypes);
}
if (logger.isDebugEnabled()) {
logger.debug("No match for " + acceptableTypes + ", supported: " + producibleTypes);
}
return;
}

MediaType.sortBySpecificityAndQuality(mediaTypesToUse);

// 遍历拿到第一个可以执行的MediaType
for (MediaType mediaType : mediaTypesToUse) {
if (mediaType.isConcrete()) {
selectedMediaType = mediaType;
break;
}
else if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) {
selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
break;
}
}

if (logger.isDebugEnabled()) {
logger.debug("Using '" + selectedMediaType + "', given " +
acceptableTypes + " and supported " + producibleTypes);
}
}

if (selectedMediaType != null) {
selectedMediaType = selectedMediaType.removeQualityValue();
// 轮询HttpMessageConverter拿到可以应用于当前MediaType和返回值的转换器
for (HttpMessageConverter<?> converter : this.messageConverters) {
GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ?
(GenericHttpMessageConverter<?>) converter : null);
// 判断是否可写,此时拿到MappingJackson2HttpMessageConverter
if (genericConverter != null ?
((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) :
converter.canWrite(valueType, selectedMediaType)) {
body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType,
(Class<? extends HttpMessageConverter<?>>) converter.getClass(),
inputMessage, outputMessage);
if (body != null) {
Object theBody = body;
LogFormatUtils.traceDebug(logger, traceOn ->
"Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]");
// 如果是下载文件新增必要的HTTP头信息
addContentDispositionHeader(inputMessage, outputMessage);
if (genericConverter != null) {
// 写出
genericConverter.write(body, targetType, selectedMediaType, outputMessage);
}
else {
((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage);
}
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Nothing to write: null body");
}
}
return;
}
}
}

if (body != null) {
if (isContentTypePreset) {
throw new HttpMessageNotWritableException(
"No converter for [" + valueType + "] with preset Content-Type '" + contentType + "'");
}
throw new HttpMediaTypeNotAcceptableException(this.allSupportedMediaTypes);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Override
public final void write(final T t, @Nullable final Type type, @Nullable MediaType contentType,
HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {

final HttpHeaders headers = outputMessage.getHeaders();
addDefaultHeaders(headers, t, contentType);

// 判断是否支持流信息写出
if (outputMessage instanceof StreamingHttpOutputMessage) {
StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
streamingOutputMessage.setBody(outputStream -> writeInternal(t, type, new HttpOutputMessage() {
@Override
public OutputStream getBody() {
return outputStream;
}
@Override
public HttpHeaders getHeaders() {
return headers;
}
}));
}
else {
// 写出并刷新
writeInternal(t, type, outputMessage);
outputMessage.getBody().flush();
}
}

使用 JSON 流的形式写出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@Override
protected void writeInternal(Object object, @Nullable Type type, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {

MediaType contentType = outputMessage.getHeaders().getContentType();
JsonEncoding encoding = getJsonEncoding(contentType);
JsonGenerator generator = this.objectMapper.getFactory().createGenerator(outputMessage.getBody(), encoding);
try {
writePrefix(generator, object);

Object value = object;
Class<?> serializationView = null;
FilterProvider filters = null;
JavaType javaType = null;

if (object instanceof MappingJacksonValue) {
MappingJacksonValue container = (MappingJacksonValue) object;
value = container.getValue();
serializationView = container.getSerializationView();
filters = container.getFilters();
}
if (type != null && TypeUtils.isAssignable(type, value.getClass())) {
javaType = getJavaType(type, null);
}

ObjectWriter objectWriter = (serializationView != null ?
this.objectMapper.writerWithView(serializationView) : this.objectMapper.writer());
if (filters != null) {
objectWriter = objectWriter.with(filters);
}
if (javaType != null && javaType.isContainerType()) {
objectWriter = objectWriter.forType(javaType);
}
SerializationConfig config = objectWriter.getConfig();
if (contentType != null && contentType.isCompatibleWith(MediaType.TEXT_EVENT_STREAM) &&
config.isEnabled(SerializationFeature.INDENT_OUTPUT)) {
objectWriter = objectWriter.with(this.ssePrettyPrinter);
}
// 写入JSON值
objectWriter.writeValue(generator, value);

writeSuffix(generator, object);
generator.flush();
}
catch (InvalidDefinitionException ex) {
throw new HttpMessageConversionException("Type definition error: " + ex.getType(), ex);
}
catch (JsonProcessingException ex) {
throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getOriginalMessage(), ex);
}
}

doDispatch直接其他善后操作

1
2
3
4
5
6
7
8
9
10
11
12
13
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
// ...
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

if (asyncManager.isConcurrentHandlingStarted()) {
return;
}

applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
// ...
}

后面的工作就是轮询拦截器执行后处理的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Apply postHandle methods of registered interceptors.
*/
void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv)
throws Exception {

HandlerInterceptor[] interceptors = getInterceptors();
if (!ObjectUtils.isEmpty(interceptors)) {
for (int i = interceptors.length - 1; i >= 0; i--) {
HandlerInterceptor interceptor = interceptors[i];
// 调用我们写的拦截器
interceptor.postHandle(request, response, this.handler, mv);
}
}
}

ServletInvocableHandlerMethod处理请求调用

拿到调用的 ServletWebRequest 以及 ModelAndViewContainer 开始处理请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {

// 转换请求参数到目标方法,调用方法并拿到返回值处理
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
// 处理相应状态
setResponseStatus(webRequest);

if (returnValue == null) {
if (isRequestNotModified(webRequest) getResponseStatus() != null mavContainer.isRequestHandled()) {
disableContentCachingIfNecessary(webRequest);
mavContainer.setRequestHandled(true);
return;
}
}
else if (StringUtils.hasText(getResponseStatusReason())) {
mavContainer.setRequestHandled(true);
return;
}

mavContainer.setRequestHandled(false);
Assert.state(this.returnValueHandlers != null, "No return value handlers");
try {
// 返回值处理器将控制器的返回值处理成客户端期待的协议,比如JSON
this.returnValueHandlers.handleReturnValue(
returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
}
catch (Exception ex) {
if (logger.isTraceEnabled()) {
logger.trace(formatErrorForReturnValue(returnValue), ex);
}
throw ex;
}
}

转换请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
@Nullable
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {

Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
if (logger.isTraceEnabled()) {
logger.trace("Arguments: " + Arrays.toString(args));
}
return doInvoke(args);
}
// 获取方法的参数签名
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {

// 当前没有请求参数,直接返回空的参数值
MethodParameter[] parameters = getMethodParameters();
if (ObjectUtils.isEmpty(parameters)) {
return EMPTY_ARGS;
}

Object[] args = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) {
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
args[i] = findProvidedArgument(parameter, providedArgs);
if (args[i] != null) {
continue;
}
if (!this.resolvers.supportsParameter(parameter)) {
throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
}
try {
// 调用处理器翻译请求参数到JavaBean
// 里层就是轮询所有翻译器,返回能够执行的进行调用
args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
}
catch (Exception ex) {
// Leave stack trace for later, exception may actually be resolved and handled...
if (logger.isDebugEnabled()) {
String exMsg = ex.getMessage();
if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
logger.debug(formatArgumentError(parameter, exMsg));
}
}
throw ex;
}
}
return args;
}

通过反射调用控制器方法执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Nullable
protected Object doInvoke(Object... args) throws Exception {
ReflectionUtils.makeAccessible(getBridgedMethod());
try {
// 传递当前对象以及参数执行,拿到返回值进行返回
return getBridgedMethod().invoke(getBean(), args);
}
catch (IllegalArgumentException ex) {
assertTargetBean(getBridgedMethod(), getBean(), args);
String text = (ex.getMessage() != null ? ex.getMessage() : "Illegal argument");
throw new IllegalStateException(formatInvokeError(text, args), ex);
}
catch (InvocationTargetException ex) {
// Unwrap for HandlerExceptionResolvers ...
Throwable targetException = ex.getTargetException();
if (targetException instanceof RuntimeException) {
throw (RuntimeException) targetException;
}
else if (targetException instanceof Error) {
throw (Error) targetException;
}
else if (targetException instanceof Exception) {
throw (Exception) targetException;
}
else {
throw new IllegalStateException(formatInvokeError("Invocation failure", args), targetException);
}
}
}

HandlerMethodReturnValueHandlerComposite处理返回值写出

循环所有消息处理器,拿到第一个支持的 HandlerMethodReturnValueHandler 调用处理器将结果写出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {

// 拿到RequestResponseBodyMethodProcessor处理器进行结果处理
HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
if (handler == null) {
throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
}
handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
}

@Nullable
private HandlerMethodReturnValueHandler selectHandler(@Nullable Object value, MethodParameter returnType) {
boolean isAsyncValue = isAsyncReturnValue(value, returnType);
for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
if (isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler)) {
continue;
}
if (handler.supportsReturnType(returnType)) {
return handler;
}
}
return null;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// AbstractMessageConverterMethodProcessor.java
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {

mavContainer.setRequestHandled(true);
// 重新封装Request和Response
ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);

// 父级方法,执行消息转换
writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}

获取返回值,根据 HTTPHeaders 进行匹配转换器,转换写入 Response 流中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType,
ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {

Object body;
Class<?> valueType;
Type targetType;

// 判断返回值
if (value instanceof CharSequence) {
body = value.toString();
valueType = String.class;
targetType = String.class;
}
else {
body = value;
valueType = getReturnValueType(body, returnType);
targetType = GenericTypeResolver.resolveType(getGenericType(returnType), returnType.getContainingClass());
}

// 是不是写出流,用于文件下载
if (isResourceType(value, returnType)) {
outputMessage.getHeaders().set(HttpHeaders.ACCEPT_RANGES, "bytes");
if (value != null && inputMessage.getHeaders().getFirst(HttpHeaders.RANGE) != null &&
outputMessage.getServletResponse().getStatus() == 200) {
Resource resource = (Resource) value;
try {
List<HttpRange> httpRanges = inputMessage.getHeaders().getRange();
outputMessage.getServletResponse().setStatus(HttpStatus.PARTIAL_CONTENT.value());
body = HttpRange.toResourceRegions(httpRanges, resource);
valueType = body.getClass();
targetType = RESOURCE_REGION_LIST_TYPE;
}
catch (IllegalArgumentException ex) {
outputMessage.getHeaders().set(HttpHeaders.CONTENT_RANGE, "bytes */" + resource.contentLength());
outputMessage.getServletResponse().setStatus(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE.value());
}
}
}

// 根据Content-Type或Accept进行匹配转换器
MediaType selectedMediaType = null;
MediaType contentType = outputMessage.getHeaders().getContentType();
boolean isContentTypePreset = contentType != null && contentType.isConcrete();
if (isContentTypePreset) {
if (logger.isDebugEnabled()) {
logger.debug("Found 'Content-Type:" + contentType + "' in response");
}
selectedMediaType = contentType;
}
else {
HttpServletRequest request = inputMessage.getServletRequest();
// 客户端请求的MediaType
List<MediaType> acceptableTypes = getAcceptableMediaTypes(request);
// 项目允许的MediaType
List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);

if (body != null && producibleTypes.isEmpty()) {
throw new HttpMessageNotWritableException(
"No converter found for return value of type: " + valueType);
}
// 轮询获取可以使用的MediaType
List<MediaType> mediaTypesToUse = new ArrayList<>();
for (MediaType requestedType : acceptableTypes) {
for (MediaType producibleType : producibleTypes) {
if (requestedType.isCompatibleWith(producibleType)) {
mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));
}
}
}
if (mediaTypesToUse.isEmpty()) {
if (body != null) {
throw new HttpMediaTypeNotAcceptableException(producibleTypes);
}
if (logger.isDebugEnabled()) {
logger.debug("No match for " + acceptableTypes + ", supported: " + producibleTypes);
}
return;
}

MediaType.sortBySpecificityAndQuality(mediaTypesToUse);

// 遍历拿到第一个可以执行的MediaType
for (MediaType mediaType : mediaTypesToUse) {
if (mediaType.isConcrete()) {
selectedMediaType = mediaType;
break;
}
else if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) {
selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
break;
}
}

if (logger.isDebugEnabled()) {
logger.debug("Using '" + selectedMediaType + "', given " +
acceptableTypes + " and supported " + producibleTypes);
}
}

if (selectedMediaType != null) {
selectedMediaType = selectedMediaType.removeQualityValue();
// 轮询HttpMessageConverter拿到可以应用于当前MediaType和返回值的转换器
for (HttpMessageConverter<?> converter : this.messageConverters) {
GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ?
(GenericHttpMessageConverter<?>) converter : null);
// 判断是否可写,此时拿到MappingJackson2HttpMessageConverter
if (genericConverter != null ?
((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) :
converter.canWrite(valueType, selectedMediaType)) {
body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType,
(Class<? extends HttpMessageConverter<?>>) converter.getClass(),
inputMessage, outputMessage);
if (body != null) {
Object theBody = body;
LogFormatUtils.traceDebug(logger, traceOn ->
"Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]");
// 如果是下载文件新增必要的HTTP头信息
addContentDispositionHeader(inputMessage, outputMessage);
if (genericConverter != null) {
// 写出
genericConverter.write(body, targetType, selectedMediaType, outputMessage);
}
else {
((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage);
}
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Nothing to write: null body");
}
}
return;
}
}
}

if (body != null) {
if (isContentTypePreset) {
throw new HttpMessageNotWritableException(
"No converter for [" + valueType + "] with preset Content-Type '" + contentType + "'");
}
throw new HttpMediaTypeNotAcceptableException(this.allSupportedMediaTypes);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Override
public final void write(final T t, @Nullable final Type type, @Nullable MediaType contentType,
HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {

final HttpHeaders headers = outputMessage.getHeaders();
addDefaultHeaders(headers, t, contentType);

// 判断是否支持流信息写出
if (outputMessage instanceof StreamingHttpOutputMessage) {
StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
streamingOutputMessage.setBody(outputStream -> writeInternal(t, type, new HttpOutputMessage() {
@Override
public OutputStream getBody() {
return outputStream;
}
@Override
public HttpHeaders getHeaders() {
return headers;
}
}));
}
else {
// 写出并刷新
writeInternal(t, type, outputMessage);
outputMessage.getBody().flush();
}
}

使用 JSON 流的形式写出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@Override
protected void writeInternal(Object object, @Nullable Type type, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {

MediaType contentType = outputMessage.getHeaders().getContentType();
JsonEncoding encoding = getJsonEncoding(contentType);
JsonGenerator generator = this.objectMapper.getFactory().createGenerator(outputMessage.getBody(), encoding);
try {
writePrefix(generator, object);

Object value = object;
Class<?> serializationView = null;
FilterProvider filters = null;
JavaType javaType = null;

if (object instanceof MappingJacksonValue) {
MappingJacksonValue container = (MappingJacksonValue) object;
value = container.getValue();
serializationView = container.getSerializationView();
filters = container.getFilters();
}
if (type != null && TypeUtils.isAssignable(type, value.getClass())) {
javaType = getJavaType(type, null);
}

ObjectWriter objectWriter = (serializationView != null ?
this.objectMapper.writerWithView(serializationView) : this.objectMapper.writer());
if (filters != null) {
objectWriter = objectWriter.with(filters);
}
if (javaType != null && javaType.isContainerType()) {
objectWriter = objectWriter.forType(javaType);
}
SerializationConfig config = objectWriter.getConfig();
if (contentType != null && contentType.isCompatibleWith(MediaType.TEXT_EVENT_STREAM) &&
config.isEnabled(SerializationFeature.INDENT_OUTPUT)) {
objectWriter = objectWriter.with(this.ssePrettyPrinter);
}
// 写入JSON值
objectWriter.writeValue(generator, value);

writeSuffix(generator, object);
generator.flush();
}
catch (InvalidDefinitionException ex) {
throw new HttpMessageConversionException("Type definition error: " + ex.getType(), ex);
}
catch (JsonProcessingException ex) {
throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getOriginalMessage(), ex);
}
}

doDispatch直接其他善后操作

1
2
3
4
5
6
7
8
9
10
11
12
13
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
// ...
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

if (asyncManager.isConcurrentHandlingStarted()) {
return;
}

applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
// ...
}

后面的工作就是轮询拦截器执行后处理的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Apply postHandle methods of registered interceptors.
*/
void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv)
throws Exception {

HandlerInterceptor[] interceptors = getInterceptors();
if (!ObjectUtils.isEmpty(interceptors)) {
for (int i = interceptors.length - 1; i >= 0; i--) {
HandlerInterceptor interceptor = interceptors[i];
// 调用我们写的拦截器
interceptor.postHandle(request, response, this.handler, mv);
}
}
}

SpringBoot

OK,现代 Java 开发者应该都对 SpringBoot 很熟悉了吧,一个很 "轻量级"IOC 容器。 我记得 SpringBoot 刚出来的时候,很多博客文章都会说 SpringBoot 减轻了 Java 开发工作者的负担,是个轻量级的框架。然而后面我才发现,并不轻量级。因为,SpringBoot 把需要依赖的东西给封装了起来,但其实比起之前自己控制依赖项目来说,反而会更重了一些,毕竟以前还可以自由组合。现在,SpringBoot 以及框架作者都提供了默认的依赖以及默认的配置,所以说这个框架轻量级其实并不是,要说轻量级应该只是说开发轻量级而已,菜鸟也可以快速上手建立一个后台项目而不必去关心太多项目配置的东西。 框架肯定是一个优秀的框架,我们项目全体也都是 SpringBoot 架构起来的,所以还是需要看看,SpringBoot 偷偷帮我们做了什么事情。

依赖简化

日常使用中,我们只需要引入一个 starter,就可以神奇把我们整合的框架整合起来。但是在以前,我们要使用 MyBatis 的时候,却需要引入 org.mybatis:mybatis 主框架,然后还因为需要整合 Spring 框架,所以我们还需要引入一个 org.mybatis:mybatis-spring。这就算了,这两个包如果版本号对不上,还要出现兼容性的问题。 然而现在我们只需要一个 org.mybatis.spring.boot:mybatis-spring-boot-starter,就可以同时引入这两个包,而且版本号还解决的很OK。所以现在我们可以看看他的 pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot</artifactId>
<version>1.3.1</version>
</parent>
<artifactId>mybatis-spring-boot-starter</artifactId>
<name>mybatis-spring-boot-starter</name>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
</dependency>
</dependencies>
</project>

所以结论是,一个 starter,定义了需要依赖的包的版本,然后通过依赖传递将这些包传递到我们 starter 所在的项目上来。 目前我建立了一个项目,这个项目很简单,只是依赖了一个 spring-boot-starter-web, 我们可以在 idea 上很方便的查看依赖的所有东西:

SpringBoot

OK,现代 Java 开发者应该都对 SpringBoot 很熟悉了吧,一个很 "轻量级"IOC 容器。 我记得 SpringBoot 刚出来的时候,很多博客文章都会说 SpringBoot 减轻了 Java 开发工作者的负担,是个轻量级的框架。然而后面我才发现,并不轻量级。因为,SpringBoot 把需要依赖的东西给封装了起来,但其实比起之前自己控制依赖项目来说,反而会更重了一些,毕竟以前还可以自由组合。现在,SpringBoot 以及框架作者都提供了默认的依赖以及默认的配置,所以说这个框架轻量级其实并不是,要说轻量级应该只是说开发轻量级而已,菜鸟也可以快速上手建立一个后台项目而不必去关心太多项目配置的东西。 框架肯定是一个优秀的框架,我们项目全体也都是 SpringBoot 架构起来的,所以还是需要看看,SpringBoot 偷偷帮我们做了什么事情。

依赖简化

日常使用中,我们只需要引入一个 starter,就可以神奇把我们整合的框架整合起来。但是在以前,我们要使用 MyBatis 的时候,却需要引入 org.mybatis:mybatis 主框架,然后还因为需要整合 Spring 框架,所以我们还需要引入一个 org.mybatis:mybatis-spring。这就算了,这两个包如果版本号对不上,还要出现兼容性的问题。 然而现在我们只需要一个 org.mybatis.spring.boot:mybatis-spring-boot-starter,就可以同时引入这两个包,而且版本号还解决的很OK。所以现在我们可以看看他的 pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot</artifactId>
<version>1.3.1</version>
</parent>
<artifactId>mybatis-spring-boot-starter</artifactId>
<name>mybatis-spring-boot-starter</name>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
</dependency>
</dependencies>
</project>

所以结论是,一个 starter,定义了需要依赖的包的版本,然后通过依赖传递将这些包传递到我们 starter 所在的项目上来。 目前我建立了一个项目,这个项目很简单,只是依赖了一个 spring-boot-starter-web, 我们可以在 idea 上很方便的查看依赖的所有东西:

但其实这个做法在以前 Spring 的时候是已经存在了,这个项目就是 Spring-IO,但我也不知道为什么,可能因为名字取得不够好吧?然后这个项目好像很少人用。或者以前的人觉得会被依赖很多东西进来所以不用这个项目了?

自动装配

我们项目所需要的依赖都弄进来了,下一步就是配置让这些包可以相互配合,共同提供服务呀。 所以,SpringBoot 所提供的第二个功能就是,根据默认的配置,装配依赖进来包里面的 Bean 实例。依赖+装配,就是我们以前一直所需要的操作了,可以说这个框架减少了全世界程序员按 CTRL+C/V 的次数hhhhh。 OK,简单了解的话,我需要一个额外的 starter,这里就使用 org.mybatis.spring.boot:mybatis-spring-boot-starter 来看,仓库地址 项目的目录结构为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
├── LICENSE
├── README.md
├── license.txt
├── mvnw
├── mvnw.cmd
├── mybatis-spring-boot-autoconfigure
│   ├── format.xml
│   ├── license.txt
│   ├── pom.xml
│   └── src
├── mybatis-spring-boot-samples
│   ├── mybatis-spring-boot-sample-annotation
│   ├── mybatis-spring-boot-sample-freemarker
│   ├── mybatis-spring-boot-sample-freemarker-legacy
│   ├── mybatis-spring-boot-sample-groovy
│   ├── mybatis-spring-boot-sample-kotlin
│   ├── mybatis-spring-boot-sample-thymeleaf
│   ├── mybatis-spring-boot-sample-velocity
│   ├── mybatis-spring-boot-sample-velocity-legacy
│   ├── mybatis-spring-boot-sample-war
│   ├── mybatis-spring-boot-sample-web
│   ├── mybatis-spring-boot-sample-xml
│   └── pom.xml
├── mybatis-spring-boot-starter
│   ├── license.txt
│   └── pom.xml
├── mybatis-spring-boot-starter-test
│   └── pom.xml
├── mybatis-spring-boot-test-autoconfigure
│   ├── format.xml
│   ├── pom.xml
│   └── src
├── pom.xml
└── travis
├── after_success.sh
└── settings.xml

其中,mybatis-spring-boot-autoconfigure 比较惹人注目,所以我们现在就看看这个项目。 首先我们看看 main/resources/META-INF/spring.factories,因为 Spring 很喜欢通过这些 meta 文件来促使各个模块很好的解耦但又能彼此配合工作,所以这个文件是定义 自动装配 开始的工厂类:

1
2
3
4
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration,\
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration

那么通过看初步的源码,这两个类都是熟悉的 Java 装配类,那么将会被加入到前面提到的 BeanFactory 容器中,后面解析将会调用里面的配置方法。 第二个优秀的地方就是,定义配置类,就可以在 yaml 文件中提示的出现,可以利用 IDE 工具很好的防止配置名写错。这个类就是 MybatisProperties.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@ConfigurationProperties(prefix = MybatisProperties.MYBATIS_PREFIX)
public class MybatisProperties {

public static final String MYBATIS_PREFIX = "mybatis";

private static final ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();

/**
* Location of MyBatis xml config file.
*/
private String configLocation;

/**
* Locations of MyBatis mapper files.
*/
private String[] mapperLocations;

...
}

OK,我们可以看到,定义了前缀就是 mybatis,而这个类是个贫血型的 Bean,只有属性。这些属性将会贯穿 MyBatis 在项目中整个生命周期。 接下来一个问题,我并不需要每个属性都在 yaml 文件中去定义啊,有些直接使用官方提供的默认值就可以了。 所以官方又贴心的提供了 additional-spring-configuration-metadata.json 这个 JSON 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
{
"properties": [
{
"sourceType": "org.apache.ibatis.session.Configuration",
"defaultValue": "org.apache.ibatis.scripting.xmltags.XMLLanguageDriver",
"name": "mybatis.configuration.default-scripting-language",
"description": "A default LanguageDriver class.",
"type": "java.lang.Class<? extends org.apache.ibatis.scripting.LanguageDriver>",
"deprecation": {
"reason": "Because when this configuration property is used, there is case that custom language driver cannot be registered correctly.",
"replacement": "mybatis.default-scripting-language-driver"
}
},
{
"sourceType": "org.apache.ibatis.session.Configuration",
"defaultValue": "org.apache.ibatis.type.EnumTypeHandler",
"name": "mybatis.configuration.default-enum-type-handler",
"description": "A default TypeHandler class for Enum.",
"type": "java.lang.Class<? extends org.apache.ibatis.type.TypeHandler>"
},
{
"defaultValue": false,
"name": "mybatis.lazy-initialization",
"description": "Set whether enable lazy initialization for mapper bean.",
"type": "java.lang.Boolean"
},
{
"name": "mybatis.scripting-language-driver.velocity.userdirective",
"deprecation": {
"level": "error",
"reason": "The 'userdirective' is deprecated since Velocity 2.x. This property defined for keeping backward compatibility with older velocity version.",
"replacement": "mybatis.scripting-language-driver.velocity.velocity-settings.runtime.custom_directives"
}
}

]
}

不好理解的话,我们直接看第三个默认配置就好了:

1
2
3
4
5
6
{
"defaultValue": false,
"name": "mybatis.lazy-initialization",
"description": "Set whether enable lazy initialization for mapper bean.",
"type": "java.lang.Boolean"
}

看了下图应该明白了吧,上面那个文件就是定义配置的一些说明、默认值的。

所以当我们要造一个框架,又因为很多约定的东西,靠人脑来记已经靠不住的情况下,就可以编写类似于 additional-spring-configuration-metadata.json 这种文件来做约定以及说明了。


OK,上面说完了 SpringBoot 的两个最主要的优点以后,现在就来看看源码。但是源码这块,因为 传递依赖 利用的是 mvn/gradle 的特性,所以 依赖传递 并不需要再说。 那么最主要的就是来解析,怎么自动装配的问题。

简单的SpringBoot项目

pom.xml 依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.BUILD-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.liweidan.web</groupId>
<artifactId>web-test</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>web-test</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

一个主启动器:

1
2
3
4
5
6
7
8
@SpringBootApplication
public class WebTestApplication {

public static void main(String[] args) {
SpringApplication.run(WebTestApplication.class, args);
}

}

一个控制器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
public class UserController {

@GetMapping("users")
public List<Map<String, String>> users() {
List<Map<String, String>> users = new ArrayList<>();
for (int i = 0; i < 3; i++) {
Map<String, String> u = new HashMap<>();
u.put("id", String.valueOf(i));
u.put("name", "name" + i);
users.add(u);
}
return users;
}

}

SpringApplication元信息准备

写过 SpringBoot 项目的同学应该对这个很熟悉了,通过传递一个上下文的根类,SpringBoot 将会自动装载在此类所在的包下面的所有类,并且 args 很明显就是我们在控制台传递的参数,也一并传递给 SpringApplication.run 这个方法,SpringBoot 即可将命令行的参数配置覆盖配置文件指定的配置。 接下来看看这个类做了什么事情:

1
2
3
4
5
6
7
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
return run(new Class<?>[] { primarySource }, args);
}

public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
return new SpringApplication(primarySources).run(args);
}

通过静态方法,传递一个 Class 类作为主资源,然后再传递给主资源数组的 run 方法。那么这个数组其实我们是可以传递多个主资源的,比如我们做项目的时候,想要每个模块包彼此分离,即可传递多个包的主资源路径。 new SpringApplication(primarySources).run(args); 才是真正的进入容器的准备阶段: 首先看看构造器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
// 初始化主资源链表,用于下面需要读取的时候可以遍历.
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
// 判断Web环境,有webFlux/web/普通三个环境,主要通过类路径是否带有相对应需要的类来判断,如果都没有则初始化为普通Java项目.
this.webApplicationType = WebApplicationType.deduceFromClasspath();
// 初始化环境,这个接口多用于web环境,因为需要从web上下文加载一些信息.
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
// 初始化监听器,监听容器生命周期中需要回调的函数
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
// 从运行堆栈中寻找运行这个类的main方法所在的类
this.mainApplicationClass = deduceMainApplicationClass();
}

加载不同模块的元信息

在上面的构造器中看到 setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));Spring 还是跟以前获取 handlers 一样。通过在 META-INF 类加载路径定义不同的 spring.factories,使用类加载器读取这些配置文件资源,解析,加载配置文件中的类,初始化对象,来共同完成业务。

1
2
3
4
5
6
7
8
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
ClassLoader classLoader = getClassLoader();
// Use names and ensure unique to protect against duplicates
Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
AnnotationAwareOrderComparator.sort(instances);
return instances;
}

通过 SpringFactoriesLoader.loadFactoryNames(type, classLoader),加载当前 ClassLoader 中的指定类,源码显示如何读取,后面的 getOrDefault(factoryTypeName, Collections.emptyList()) 还是比较好理解的,如果加载到工厂类的名字就返回不然返回个空集合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
String factoryTypeName = factoryType.getName();
return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
}

private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
// 相当于Map<String, List<String>>结构,先从缓存命中.
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null) {
return result;
}

/*
属性定义了元信息的路径以及名字
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
*/
try {
// 开始加载类路径下特定名字的元信息文件
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
result = new LinkedMultiValueMap<>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
// 通过UrlResource解析成Properties对象
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
// 遍历所有模块的指定文件,然后加入缓存
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryTypeName = ((String) entry.getKey()).trim();
for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
result.add(factoryTypeName, factoryImplementationName.trim());
}
}
}
cache.put(classLoader, result);
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}

启动项目SpringApplication.run

可以看到 run 的源码跟之前的 AbstractApplicationContext#refresh 的味道还是一样的,先加载一系列容器运行时需要的生命周期类(Spring 模块间的也有用户自定义的),然后 refreshContext(context) 刷新容器上下文。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public ConfigurableApplicationContext run(String... args) {
// 这是用来打印加载时间的工具类,暂时可以略过.
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
// 主要设置JVM虚拟机支持无设备情况下让awt可运行的属性
configureHeadlessProperty();
// 一. 加载所有SpringApplicationRunListeners并开始遍历所有Lintener启动监听回调函数
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// 二. 开始准备ConfigurableEnvironment环境
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
// 三. 创建应用上下文
context = createApplicationContext();
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
// 四. 做一些准备工作
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
// 五. 刷新上下文
refreshContext(context);
// 六. 刷新完成后做的一些清理、回调工作
afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
// 七. 启动完成后,调用所有SpringApplicationRunListener的完成启动的回调函数
listeners.started(context);
// 八. 主要处理ApplicationRunner和CommandLineRunner的回调
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}

try {
// 九. 运行时的SpringApplicationRunListener回调函数
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
return context;
}

装载SpringApplicationRunListener

1
2
3
4
5
private SpringApplicationRunListeners getRunListeners(String[] args) {
Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
return new SpringApplicationRunListeners(logger,
getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args));
}

首先我们看第一步,加载所有的 SpringApplicationRunListener 子类,这个加载方式跟上面所说的加载元信息是一样的,只不过指定加载 SpringApplicationRunListener.class 类。 我们知道,Spring 经常会定义很多生命周期回调,供用户根据需求切入框架。这次 SpringBoot 的生命周期是 SpringApplicationRunListener。 首先看看 SpringApplicationRunListener 定义哪些生命周期函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public interface SpringApplicationRunListener {

// 项目开始时调用
default void starting() {
}

// 环境准备好时
default void environmentPrepared(ConfigurableEnvironment environment) {
}

// 上下文准备好时
default void contextPrepared(ConfigurableApplicationContext context) {
}

// 上下文读取完成
default void contextLoaded(ConfigurableApplicationContext context) {
}

// 启动完成
default void started(ConfigurableApplicationContext context) {
}

// 项目运行时调用
default void running(ConfigurableApplicationContext context) {
}

// 项目失败时
default void failed(ConfigurableApplicationContext context, Throwable exception) {
}

}

准备ConfigurableEnvironment环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments) {
// 首先根据环境类型获取对象的配置实现对象
ConfigurableEnvironment environment = getOrCreateEnvironment();
// 配置ConversionService单例对象,以及判断有没有设置Profiles环境
configureEnvironment(environment, applicationArguments.getSourceArgs());
// 读取其他环境中(比如web.xml)读取的配置属性
ConfigurationPropertySources.attach(environment);
// 继续上面的SpringApplicationRunListener生命周期调用
listeners.environmentPrepared(environment);
// 把环境绑定到Binder中,Binder是Spring提供的一个记录对象的容器
bindToSpringApplication(environment);
if (!this.isCustomEnvironment) {
// 如果当前的配置环境和重新判断的环境不同,则转换成当前的环境(有可能在生命周期修改了加载的环境?)
environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
deduceEnvironmentClass());
}
// 重新读取特定环境的配置
ConfigurationPropertySources.attach(environment);
return environment;
}

private ConfigurableEnvironment getOrCreateEnvironment() {
if (this.environment != null) {
return this.environment;
}
switch (this.webApplicationType) {
case SERVLET:
return new StandardServletEnvironment();
case REACTIVE:
return new StandardReactiveWebEnvironment();
default:
return new StandardEnvironment();
}
}

然后在主线程中配置忽略 BooleanBean

1
2
3
4
5
6
7
8
9
10
11
12
13
public ConfigurableApplicationContext run(String... args) {
//...
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);
//...
}

private void configureIgnoreBeanInfo(ConfigurableEnvironment environment) {
if (System.getProperty(CachedIntrospectionResults.IGNORE_BEANINFO_PROPERTY_NAME) == null) {
Boolean ignore = environment.getProperty("spring.beaninfo.ignore", Boolean.class, Boolean.TRUE);
System.setProperty(CachedIntrospectionResults.IGNORE_BEANINFO_PROPERTY_NAME, ignore.toString());
}
}

创建上下文

1
2
3
4
context = createApplicationContext();
// 获取模块的错误解释器,用于项目启动的时候解析是什么错误
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);

根据不同环境创建不同的上下文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
protected ConfigurableApplicationContext createApplicationContext() {
Class<?> contextClass = this.applicationContextClass;
if (contextClass == null) {
try {
switch (this.webApplicationType) {
case SERVLET:
contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);
break;
case REACTIVE:
contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
break;
default:
contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
}
}
catch (ClassNotFoundException ex) {
throw new IllegalStateException(
"Unable create a default ApplicationContext, please specify an ApplicationContextClass", ex);
}
}
return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
}

Context准备工作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment,
SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
// 设置上面准备好的环境信息
context.setEnvironment(environment);
// 处理之前做一些事情,但是当前环境下,只是注册了一个 ConversionService 到 BeanFactory 中
postProcessApplicationContext(context);
// 调用之前注册的所有Initializers
applyInitializers(context);
// 再调用所有的listeners
listeners.contextPrepared(context);
if (this.logStartupInfo) {
logStartupInfo(context.getParent() == null);
logStartupProfileInfo(context);
}
// 注册SpringBoot独有的一些Bean
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
if (printedBanner != null) {
beanFactory.registerSingleton("springBootBanner", printedBanner);
}
// 接下来跟之前一样
if (beanFactory instanceof DefaultListableBeanFactory) {
((DefaultListableBeanFactory) beanFactory)
.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
}
if (this.lazyInitialization) {
context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
}
// Load the sources
Set<Object> sources = getAllSources();
Assert.notEmpty(sources, "Sources must not be empty");
// 根据我们传递的主类进行读取,放入BeanFactory中
load(context, sources.toArray(new Object[0]));
listeners.contextLoaded(context);
}

其实当前 postProcessApplicationContext 除了注册 ConversionService 以外其他事情都没做。

refreshContext刷新上下文

因为我要看看自动装配的问题,所以这个时候为了故事比较好说,我加了 mybatis-spring-boot-starter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.BUILD-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.liweidan.web</groupId>
<artifactId>web-test</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>web-test</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.197</version>
</dependency>

</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

刷新上下文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void refreshContext(ConfigurableApplicationContext context) {
refresh(context);
if (this.registerShutdownHook) {
try {
context.registerShutdownHook();
}
catch (AccessControlException ex) {
// Not allowed in some environments.
}
}
}
protected void refresh(ApplicationContext applicationContext) {
Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext);
((AbstractApplicationContext) applicationContext).refresh();
}

然后接下来就去到我们熟悉的 AbstractApplicationContext 中,重温一下这个 refresh() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
prepareRefresh();

// Tell the subclass to refresh the internal bean factory.
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);

try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);

// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);

// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);

// Initialize message source for this context.
initMessageSource();

// Initialize event multicaster for this context.
initApplicationEventMulticaster();

// Initialize other special beans in specific context subclasses.
onRefresh();

// Check for listener beans and register them.
registerListeners();

// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);

// Last step: publish corresponding event.
finishRefresh();
}

catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}

// Destroy already created singletons to avoid dangling resources.
destroyBeans();

// Reset 'active' flag.
cancelRefresh(ex);

// Propagate exception to caller.
throw ex;
}

finally {
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
resetCommonCaches();
}
}
}

同之前一样,在 invokeBeanFactoryPostProcessors(beanFactory) 这句话出现了 BeanDefinition 数量的剧增,那么我们可以推断出,这句话还是关键,就是在这句话开始解析我们的项目依赖。

其实按照我的猜测,自动配置应该是 starter 提供了一些配置类交给 SpringBoot 注入 Spring 的 BeanFactory 中,但是现在看来,好像 SpringBoot 就不需要做什么东西,直接委托给容器扫描解析执行了。

invokeBeanFactoryPostProcessors

1
2
3
4
5
6
7
8
9
10
11
protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
// 利用委派模式委托给 PostProcessorRegistrationDelegate 进行执行
PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());

// Detect a LoadTimeWeaver and prepare for weaving, if found in the meantime
// (e.g. through an @Bean method registered by ConfigurationClassPostProcessor)
if (beanFactory.getTempClassLoader() == null && beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) {
beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));
beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
}
}

PostProcessorRegistrationDelegate委派对象

这是一个在 Context 模块下对象,主要处理 PostProcessor 相关的事情,看到签名只提供两个方法:

  • invokeBeanFactoryPostProcessors 执行 BF 的 PostProcessors
  • registerBeanPostProcessors 注册 PostProcessors 到 BF 中

关于普通的配置类解析,之前在 chapters3_高级的Beafctory—Spring上下文 已经有提到,解析项目中配置类不明白的话可以先看看那篇文章。 不过我们现在重点是怎么读取到 starter 框架里边的自动配置信息:

1
2
3
4
5
6
7
private static void invokeBeanDefinitionRegistryPostProcessors(
Collection<? extends BeanDefinitionRegistryPostProcessor> postProcessors, BeanDefinitionRegistry registry) {

for (BeanDefinitionRegistryPostProcessor postProcessor : postProcessors) {
postProcessor.postProcessBeanDefinitionRegistry(registry);
}
}

当前 postProcessors 里边只有一个 ConfigurationClassPostProcessor 那我们进入对应的 postProcessBeanDefinitionRegistry 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
int registryId = System.identityHashCode(registry);
if (this.registriesPostProcessed.contains(registryId)) {
throw new IllegalStateException(
"postProcessBeanDefinitionRegistry already called on this post-processor against " + registry);
}
if (this.factoriesPostProcessed.contains(registryId)) {
throw new IllegalStateException(
"postProcessBeanFactory already called on this post-processor against " + registry);
}
this.registriesPostProcessed.add(registryId);

processConfigBeanDefinitions(registry);
}

processConfigBeanDefinitions 解析配置

由于我们现在需要跟自动导入的配置,所以我们应该需要跟的类配置是我们的主类,也就是 WebTestApplication 这个类,这个类上修饰的注解 @SpringBootApplication 是自动注入的关键

这个才是执行自动配置的重点之重:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
/**
* Build and validate a configuration model based on the registry of
* {@link Configuration} classes.
*/
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
List<BeanDefinitionHolder> configCandidates = new ArrayList<>();
String[] candidateNames = registry.getBeanDefinitionNames();

for (String beanName : candidateNames) {
BeanDefinition beanDef = registry.getBeanDefinition(beanName);
if (beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE) != null) {
if (logger.isDebugEnabled()) {
logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);
}
}
else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
}
}

// Return immediately if no @Configuration classes were found
if (configCandidates.isEmpty()) {
return;
}

// Sort by previously determined @Order value, if applicable
configCandidates.sort((bd1, bd2) -> {
int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition());
int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition());
return Integer.compare(i1, i2);
});

// Detect any custom bean name generation strategy supplied through the enclosing application context
SingletonBeanRegistry sbr = null;
if (registry instanceof SingletonBeanRegistry) {
sbr = (SingletonBeanRegistry) registry;
if (!this.localBeanNameGeneratorSet) {
BeanNameGenerator generator = (BeanNameGenerator) sbr.getSingleton(
AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR);
if (generator != null) {
this.componentScanBeanNameGenerator = generator;
this.importBeanNameGenerator = generator;
}
}
}

if (this.environment == null) {
this.environment = new StandardEnvironment();
}

// Parse each @Configuration class
ConfigurationClassParser parser = new ConfigurationClassParser(
this.metadataReaderFactory, this.problemReporter, this.environment,
this.resourceLoader, this.componentScanBeanNameGenerator, registry);

Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());
// ↑ 这上面属于解析开始的准备工作 ↑
do {
parser.parse(candidates);
parser.validate();

Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
configClasses.removeAll(alreadyParsed);

// Read the model and create bean definitions based on its content
if (this.reader == null) {
this.reader = new ConfigurationClassBeanDefinitionReader(
registry, this.sourceExtractor, this.resourceLoader, this.environment,
this.importBeanNameGenerator, parser.getImportRegistry());
}
this.reader.loadBeanDefinitions(configClasses);
alreadyParsed.addAll(configClasses);

candidates.clear();
if (registry.getBeanDefinitionCount() > candidateNames.length) {
String[] newCandidateNames = registry.getBeanDefinitionNames();
Set<String> oldCandidateNames = new HashSet<>(Arrays.asList(candidateNames));
Set<String> alreadyParsedClasses = new HashSet<>();
for (ConfigurationClass configurationClass : alreadyParsed) {
alreadyParsedClasses.add(configurationClass.getMetadata().getClassName());
}
for (String candidateName : newCandidateNames) {
if (!oldCandidateNames.contains(candidateName)) {
BeanDefinition bd = registry.getBeanDefinition(candidateName);
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, this.metadataReaderFactory) &&
!alreadyParsedClasses.contains(bd.getBeanClassName())) {
candidates.add(new BeanDefinitionHolder(bd, candidateName));
}
}
}
candidateNames = newCandidateNames;
}
}
while (!candidates.isEmpty());

// Register the ImportRegistry as a bean in order to support ImportAware @Configuration classes
if (sbr != null && !sbr.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) {
sbr.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry());
}

if (this.metadataReaderFactory instanceof CachingMetadataReaderFactory) {
// Clear cache in externally provided MetadataReaderFactory; this is a no-op
// for a shared cache since it'll be cleared by the ApplicationContext.
((CachingMetadataReaderFactory) this.metadataReaderFactory).clearCache();
}
}

重点看看 parser.parse(candidates); 这句话是怎么解析的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public void parse(Set<BeanDefinitionHolder> configCandidates) {
for (BeanDefinitionHolder holder : configCandidates) {
BeanDefinition bd = holder.getBeanDefinition();
try {
if (bd instanceof AnnotatedBeanDefinition) {
// 因为这是一个使用注解的Bean,所以应该关注这里
parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
}
else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) {
parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName());
}
else {
parse(bd.getBeanClassName(), holder.getBeanName());
}
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex);
}
}

this.deferredImportSelectorHandler.process();
}

protected final void parse(AnnotationMetadata metadata, String beanName) throws IOException {
processConfigurationClass(new ConfigurationClass(metadata, beanName));
}

protected void processConfigurationClass(ConfigurationClass configClass) throws IOException {
// 如果当前的类是一个解析类,则跳过解析
if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {
return;
}

// 判断是否已经解析过了
ConfigurationClass existingClass = this.configurationClasses.get(configClass);
if (existingClass != null) {
if (configClass.isImported()) {
if (existingClass.isImported()) {
existingClass.mergeImportedBy(configClass);
}
// Otherwise ignore new imported config class; existing non-imported class overrides it.
return;
}
else {
// Explicit bean definition found, probably replacing an import.
// Let's remove the old one and go with the new one.
this.configurationClasses.remove(configClass);
this.knownSuperclasses.values().removeIf(configClass::equals);
}
}

// 递归解析配置类以及他的父类、接口等等
SourceClass sourceClass = asSourceClass(configClass);
do {
sourceClass = doProcessConfigurationClass(configClass, sourceClass);
}
while (sourceClass != null);

this.configurationClasses.put(configClass, configClass);
}

然后我们要进入 doProcessConfigurationClass 观察解析的过程。 这串代码其实有点长……:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
@Nullable
protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)
throws IOException {

if (configClass.getMetadata().isAnnotated(Component.class.getName())) {
// Recursively process any member (nested) classes first
processMemberClasses(configClass, sourceClass);
}

// 解析需要导入Property配置文件的类
for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), PropertySources.class,
org.springframework.context.annotation.PropertySource.class)) {
if (this.environment instanceof ConfigurableEnvironment) {
processPropertySource(propertySource);
}
else {
logger.info("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +
"]. Reason: Environment must implement ConfigurableEnvironment");
}
}

// 解析 @ComponentScan
Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
if (!componentScans.isEmpty() &&
!this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
for (AnnotationAttributes componentScan : componentScans) {
// The config class is annotated with @ComponentScan -> perform the scan immediately
Set<BeanDefinitionHolder> scannedBeanDefinitions =
this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
// Check the set of scanned definitions for any further config classes and parse recursively if needed
for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
if (bdCand == null) {
bdCand = holder.getBeanDefinition();
}
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
parse(bdCand.getBeanClassName(), holder.getBeanName());
}
}
}
}

// 解析 @Import 的类,这句话是重点
processImports(configClass, sourceClass, getImports(sourceClass), true);

// Process any @ImportResource annotations
AnnotationAttributes importResource =
AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class);
if (importResource != null) {
String[] resources = importResource.getStringArray("locations");
Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader");
for (String resource : resources) {
String resolvedResource = this.environment.resolveRequiredPlaceholders(resource);
configClass.addImportedResource(resolvedResource, readerClass);
}
}

// 解析 @Bean 方法
Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);
for (MethodMetadata methodMetadata : beanMethods) {
configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
}

// 解析接口默认方法
processInterfaces(configClass, sourceClass);

// 如果有父类,返回父类,由上一层继续调用这个方法继续解析
if (sourceClass.getMetadata().hasSuperClass()) {
String superclass = sourceClass.getMetadata().getSuperClassName();
if (superclass != null && !superclass.startsWith("java") &&
!this.knownSuperclasses.containsKey(superclass)) {
this.knownSuperclasses.put(superclass, configClass);
// Superclass found, return its annotation metadata and recurse
return sourceClass.getSuperClass();
}
}

// 返回Null表示解析结束
return null;
}

好了,可以看到通过 Java 类的解析可谓覆盖到尼玛我想象不到的地方….但他们就是应该要有 挑重点来看吧,我的主类没有父级也没有接口,直接重点看 processImports(configClass, sourceClass, getImports(sourceClass), true) 这句话。 这句话首先需要获取所有的导入配置 getImports(sourceClass)

1
2
3
4
5
6
private Set<SourceClass> getImports(SourceClass sourceClass) throws IOException {
Set<SourceClass> imports = new LinkedHashSet<>();
Set<SourceClass> visited = new LinkedHashSet<>();
collectImports(sourceClass, imports, visited);
return imports;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
private void collectImports(SourceClass sourceClass, Set<SourceClass> imports, Set<SourceClass> visited)
throws IOException {

if (visited.add(sourceClass)) {
for (SourceClass annotation : sourceClass.getAnnotations()) {
String annName = annotation.getMetadata().getClassName();
if (!annName.equals(Import.class.getName())) {
collectImports(annotation, imports, visited);
}
}
imports.addAll(sourceClass.getAnnotationAttributes(Import.class.getName(), "value"));
}
}

这两个东西,切忌不要去 debug 一个一个看….我在这里看晕了好几个小时,应该是条件断点判断 sourceClass.getAnnotationAttributes(Import.class.getName(), "value").size() > 0 再停住

好了等到断点停住的时候,发现是 @interface SpringBootApplication > @interface EnableAutoConfiguration 上面的 @Import(AutoConfigurationImportSelector.class) 在这里导入了。以及这个类上边的 @AutoConfigurationPackage 导入了 @Import(AutoConfigurationPackages.Registrar.class) 所以当前需要解析的 importCandidatesAutoConfigurationPackages.Registrar.class 以及 AutoConfigurationImportSelector.class 两个类。 那么这两个类是什么用的,我现在也还不知道…先知道他被导入就好了,接下去看 processImports 方法了,这个方法应该有答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
Collection<SourceClass> importCandidates, boolean checkForCircularImports) {

if (importCandidates.isEmpty()) {
return;
}

// 由上面传递的 checkForCircularImports 参数决定是否判断有没有循环引入
if (checkForCircularImports && isChainedImportOnStack(configClass)) {
this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
}
else {
this.importStack.push(configClass);
try {
// 开始解析导入类
for (SourceClass candidate : importCandidates) {
// 第一层,根据不同的导入类型进行解析
// AutoConfigurationImportSelector 来到这个分支
if (candidate.isAssignable(ImportSelector.class)) {
// Candidate class is an ImportSelector -> delegate to it to determine imports
Class<?> candidateClass = candidate.loadClass();
ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class,
this.environment, this.resourceLoader, this.registry);
if (selector instanceof DeferredImportSelector) {
this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
}
else {
String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames);
// 递归解析
processImports(configClass, currentSourceClass, importSourceClasses, false);
}
}
// AutoConfigurationPackages.Registrar进入这个分支
else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
// Candidate class is an ImportBeanDefinitionRegistrar ->
// delegate to it to register additional bean definitions
Class<?> candidateClass = candidate.loadClass();
ImportBeanDefinitionRegistrar registrar =
ParserStrategyUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class,
this.environment, this.resourceLoader, this.registry);
configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
}
else {
// 直接以@Configuration的方式进行处理
this.importStack.registerImport(
currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
processConfigurationClass(candidate.asConfigClass(configClass));
}
}
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to process import candidates for configuration class [" +
configClass.getMetadata().getClassName() + "]", ex);
}
finally {
this.importStack.pop();
}
}
}

不行呀,因为需要根据类型来做,先给两个类的类型吧:

这三个分支对应着三个不同的配置类型:

  1. ImportSelector 用于导入配置类的信息
  2. Registrar 用于解析自定义注解动态生成 Bean 的信息
  3. @Configuration 读取到配置类,直接解析

那我们就先按照上图的顺序来解析这些配置类吧,首先是 AutoConfigurationPackages.Registrar 对象的解析。我先从上面一大串截取代码片段来看:

1
2
3
4
5
6
7
8
9
10
11
// ...
else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
// 额外注册BeanDefinition用
Class<?> candidateClass = candidate.loadClass();
ImportBeanDefinitionRegistrar registrar =
ParserStrategyUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class,
this.environment, this.resourceLoader, this.registry);
// 简单的添加(导入)到当前的 configClass 中
configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
}
// ...

先放到后面,应该是解析的时候会调用到。 接下来看第二个 AutoConfigurationImportSelector.class 的解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (candidate.isAssignable(ImportSelector.class)) {
// Candidate class is an ImportSelector -> delegate to it to determine imports
Class<?> candidateClass = candidate.loadClass();
ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class,
this.environment, this.resourceLoader, this.registry);
if (selector instanceof DeferredImportSelector) {
// 进入这里
this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
}
else {
String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames);
processImports(configClass, currentSourceClass, importSourceClasses, false);
}
}

这一块比较麻烦,在上面 uml 可以看到,AutoConfigurationImportSelector.class 属于 DeferredImportSelector,所以交给 deferredImportSelectorHandler 进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
public void handle(ConfigurationClass configClass, DeferredImportSelector importSelector) {
DeferredImportSelectorHolder holder = new DeferredImportSelectorHolder(
configClass, importSelector);
if (this.deferredImportSelectors == null) {
DeferredImportSelectorGroupingHandler handler = new DeferredImportSelectorGroupingHandler();
handler.register(holder);
handler.processGroupImports();
}
else {
// 来到这里,简单的加入
this.deferredImportSelectors.add(holder);
}
}

导入刚刚注册的配置

上面一大堆全是解析,然后接下来就要处理这些自动配置了。 回到 parser.parse(candidates); 这个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public void parse(Set<BeanDefinitionHolder> configCandidates) {
for (BeanDefinitionHolder holder : configCandidates) {
BeanDefinition bd = holder.getBeanDefinition();
try {
if (bd instanceof AnnotatedBeanDefinition) {
parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
}
else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) {
parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName());
}
else {
parse(bd.getBeanClassName(), holder.getBeanName());
}
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex);
}
}

// 处理导入配置类(第三方配置)
this.deferredImportSelectorHandler.process();
}

AutoConfigurationImportSelector解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void process() {
// AutoConfigurationImportSelector集合 只有一个元素
List<DeferredImportSelectorHolder> deferredImports = this.deferredImportSelectors;
this.deferredImportSelectors = null;
try {
if (deferredImports != null) {
DeferredImportSelectorGroupingHandler handler = new DeferredImportSelectorGroupingHandler();
deferredImports.sort(DEFERRED_IMPORT_COMPARATOR);
// 注册到 DeferredImportSelectorGroupingHandler 中
deferredImports.forEach(handler::register);
// 重点看这边
handler.processGroupImports();
}
}
finally {
this.deferredImportSelectors = new ArrayList<>();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void processGroupImports() {
for (DeferredImportSelectorGrouping grouping : this.groupings.values()) {
grouping.getImports().forEach(entry -> {
ConfigurationClass configurationClass = this.configurationClasses.get(
entry.getMetadata());
try {
processImports(configurationClass, asSourceClass(configurationClass),
asSourceClasses(entry.getImportClassName()), false);
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to process import candidates for configuration class [" +
configurationClass.getMetadata().getClassName() + "]", ex);
}
});
}
}

先进入这里 grouping.getImports()

1
2
3
4
5
6
7
public Iterable<Group.Entry> getImports() {
for (DeferredImportSelectorHolder deferredImport : this.deferredImports) {
this.group.process(deferredImport.getConfigurationClass().getMetadata(),
deferredImport.getImportSelector());
}
return this.group.selectImports();
}

这个过程就是 SpringBoot 提供的,在类路径提供 additional-spring-configuration-metadata.json 提供导入配置的,然而看起来好像所有的 SpringBoot 模块都在这里了,他只是通过导入类路径的包来判断是否要加载配置。 跳过这一段吧,看着有点累。我们现在只要知道,读取了类路径下的 additional-spring-configuration-metadata.json 后,我们想要看到的 MybatisAutoConfiguration 已经被读取到了就好了。

同样的,spring-boot-mybatis-starter 也提供了上面的注册文件:

好了,接下来回到上面的解析方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void processGroupImports() {
for (DeferredImportSelectorGrouping grouping : this.groupings.values()) {
grouping.getImports().forEach(entry -> {
ConfigurationClass configurationClass = this.configurationClasses.get(
entry.getMetadata());
try {
// 此时进入processImports的时候就不需要再导入配置了
// 直接以 @Configuration 去实现配置
processImports(configurationClass, asSourceClass(configurationClass),
asSourceClasses(entry.getImportClassName()), false);
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to process import candidates for configuration class [" +
configurationClass.getMetadata().getClassName() + "]", ex);
}
});
}
}

那么关于 @Configuration 的解析,在我 之前的文章 就可以看到了。 现在就看看怎么根据主类的所在的包,扫描该包下的所有 Bean

自动包扫描注册Bean

我们得回到 doProcessConfigurationClass 的这个地方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 解析 @ComponentScan,会从主类 WebTestApplication 中拿到 BasePackage
// 然后根据这个 BasePackage 的配置进行扫描下面的类
Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
if (!componentScans.isEmpty() &&
!this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
for (AnnotationAttributes componentScan : componentScans) {
// 通过 componentScanParser 进行解析
Set<BeanDefinitionHolder> scannedBeanDefinitions =
this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
// Check the set of scanned definitions for any further config classes and parse recursively if needed
for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
if (bdCand == null) {
bdCand = holder.getBeanDefinition();
}
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
parse(bdCand.getBeanClassName(), holder.getBeanName());
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, final String declaringClass) {
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(this.registry,
componentScan.getBoolean("useDefaultFilters"), this.environment, this.resourceLoader);

Class<? extends BeanNameGenerator> generatorClass = componentScan.getClass("nameGenerator");
boolean useInheritedGenerator = (BeanNameGenerator.class == generatorClass);
scanner.setBeanNameGenerator(useInheritedGenerator ? this.beanNameGenerator :
BeanUtils.instantiateClass(generatorClass));

ScopedProxyMode scopedProxyMode = componentScan.getEnum("scopedProxy");
if (scopedProxyMode != ScopedProxyMode.DEFAULT) {
scanner.setScopedProxyMode(scopedProxyMode);
}
else {
Class<? extends ScopeMetadataResolver> resolverClass = componentScan.getClass("scopeResolver");
scanner.setScopeMetadataResolver(BeanUtils.instantiateClass(resolverClass));
}

scanner.setResourcePattern(componentScan.getString("resourcePattern"));

for (AnnotationAttributes filter : componentScan.getAnnotationArray("includeFilters")) {
for (TypeFilter typeFilter : typeFiltersFor(filter)) {
scanner.addIncludeFilter(typeFilter);
}
}
for (AnnotationAttributes filter : componentScan.getAnnotationArray("excludeFilters")) {
for (TypeFilter typeFilter : typeFiltersFor(filter)) {
scanner.addExcludeFilter(typeFilter);
}
}

boolean lazyInit = componentScan.getBoolean("lazyInit");
if (lazyInit) {
scanner.getBeanDefinitionDefaults().setLazyInit(true);
}

Set<String> basePackages = new LinkedHashSet<>();
String[] basePackagesArray = componentScan.getStringArray("basePackages");
for (String pkg : basePackagesArray) {
String[] tokenized = StringUtils.tokenizeToStringArray(this.environment.resolvePlaceholders(pkg),
ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
Collections.addAll(basePackages, tokenized);
}
for (Class<?> clazz : componentScan.getClassArray("basePackageClasses")) {
basePackages.add(ClassUtils.getPackageName(clazz));
}
if (basePackages.isEmpty()) {
// 没有指定扫描包,进入这里获取主类的包名
basePackages.add(ClassUtils.getPackageName(declaringClass));
}

scanner.addExcludeFilter(new AbstractTypeHierarchyTraversingFilter(false, false) {
@Override
protected boolean matchClassName(String className) {
return declaringClass.equals(className);
}
});

// 上面基本上都是解析配置的一些东西,过滤器啊,LazyInit等等,到这一步才是真正的扫描
// StringUtils.toStringArray(basePackages) 获取包名
return scanner.doScan(StringUtils.toStringArray(basePackages));
}

真正的扫描,做的事情可谓多得多

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
Assert.notEmpty(basePackages, "At least one base package must be specified");
Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
for (String basePackage : basePackages) {
// 这里又是一个大的模块了,也就是读取ClassPath下所有的文件
Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
// 拿到扫描到的 BeanDefinition 注册到工厂中
for (BeanDefinition candidate : candidates) {
ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
candidate.setScope(scopeMetadata.getScopeName());
String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
if (candidate instanceof AbstractBeanDefinition) {
postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
}
if (candidate instanceof AnnotatedBeanDefinition) {
AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
}
if (checkCandidate(beanName, candidate)) {
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
definitionHolder =
AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
beanDefinitions.add(definitionHolder);
registerBeanDefinition(definitionHolder, this.registry);
}
}
}
return beanDefinitions;
}

扫描:

1
2
3
4
5
6
7
8
public Set<BeanDefinition> findCandidateComponents(String basePackage) {
if (this.componentsIndex != null && indexSupportsIncludeFilters()) {
return addCandidateComponentsFromIndex(this.componentsIndex, basePackage);
}
else {
return scanCandidateComponents(basePackage);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
Set<BeanDefinition> candidates = new LinkedHashSet<>();
try {
String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
resolveBasePackage(basePackage) + '/' + this.resourcePattern;
Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
boolean traceEnabled = logger.isTraceEnabled();
boolean debugEnabled = logger.isDebugEnabled();
// 拿到包下两个文件的 Resource 实例
for (Resource resource : resources) {
if (traceEnabled) {
logger.trace("Scanning " + resource);
}
if (resource.isReadable()) {
try {
MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
if (isCandidateComponent(metadataReader)) {
ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
sbd.setResource(resource);
sbd.setSource(resource);
// 符合某种条件,暂且就当BF中还没存在这个Bean吧
if (isCandidateComponent(sbd)) {
if (debugEnabled) {
logger.debug("Identified candidate component class: " + resource);
}
// 添加
candidates.add(sbd);
}
else {
if (debugEnabled) {
logger.debug("Ignored because not a concrete top-level class: " + resource);
}
}
}
else {
if (traceEnabled) {
logger.trace("Ignored because not matching any filter: " + resource);
}
}
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to read candidate component class: " + resource, ex);
}
}
else {
if (traceEnabled) {
logger.trace("Ignored because not readable: " + resource);
}
}
}
}
catch (IOException ex) {
throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex);
}
// 返回
return candidates;
}

OK,扫描完成,后在 finishBeanFactoryInitialization 进行初始化就可以了

结束

但其实这个做法在以前 Spring 的时候是已经存在了,这个项目就是 Spring-IO,但我也不知道为什么,可能因为名字取得不够好吧?然后这个项目好像很少人用。或者以前的人觉得会被依赖很多东西进来所以不用这个项目了?

自动装配

我们项目所需要的依赖都弄进来了,下一步就是配置让这些包可以相互配合,共同提供服务呀。 所以,SpringBoot 所提供的第二个功能就是,根据默认的配置,装配依赖进来包里面的 Bean 实例。依赖+装配,就是我们以前一直所需要的操作了,可以说这个框架减少了全世界程序员按 CTRL+C/V 的次数hhhhh。 OK,简单了解的话,我需要一个额外的 starter,这里就使用 org.mybatis.spring.boot:mybatis-spring-boot-starter 来看,仓库地址 项目的目录结构为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
├── LICENSE
├── README.md
├── license.txt
├── mvnw
├── mvnw.cmd
├── mybatis-spring-boot-autoconfigure
│   ├── format.xml
│   ├── license.txt
│   ├── pom.xml
│   └── src
├── mybatis-spring-boot-samples
│   ├── mybatis-spring-boot-sample-annotation
│   ├── mybatis-spring-boot-sample-freemarker
│   ├── mybatis-spring-boot-sample-freemarker-legacy
│   ├── mybatis-spring-boot-sample-groovy
│   ├── mybatis-spring-boot-sample-kotlin
│   ├── mybatis-spring-boot-sample-thymeleaf
│   ├── mybatis-spring-boot-sample-velocity
│   ├── mybatis-spring-boot-sample-velocity-legacy
│   ├── mybatis-spring-boot-sample-war
│   ├── mybatis-spring-boot-sample-web
│   ├── mybatis-spring-boot-sample-xml
│   └── pom.xml
├── mybatis-spring-boot-starter
│   ├── license.txt
│   └── pom.xml
├── mybatis-spring-boot-starter-test
│   └── pom.xml
├── mybatis-spring-boot-test-autoconfigure
│   ├── format.xml
│   ├── pom.xml
│   └── src
├── pom.xml
└── travis
├── after_success.sh
└── settings.xml

其中,mybatis-spring-boot-autoconfigure 比较惹人注目,所以我们现在就看看这个项目。 首先我们看看 main/resources/META-INF/spring.factories,因为 Spring 很喜欢通过这些 meta 文件来促使各个模块很好的解耦但又能彼此配合工作,所以这个文件是定义 自动装配 开始的工厂类:

1
2
3
4
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration,\
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration

那么通过看初步的源码,这两个类都是熟悉的 Java 装配类,那么将会被加入到前面提到的 BeanFactory 容器中,后面解析将会调用里面的配置方法。 第二个优秀的地方就是,定义配置类,就可以在 yaml 文件中提示的出现,可以利用 IDE 工具很好的防止配置名写错。这个类就是 MybatisProperties.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@ConfigurationProperties(prefix = MybatisProperties.MYBATIS_PREFIX)
public class MybatisProperties {

public static final String MYBATIS_PREFIX = "mybatis";

private static final ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();

/**
* Location of MyBatis xml config file.
*/
private String configLocation;

/**
* Locations of MyBatis mapper files.
*/
private String[] mapperLocations;

...
}

OK,我们可以看到,定义了前缀就是 mybatis,而这个类是个贫血型的 Bean,只有属性。这些属性将会贯穿 MyBatis 在项目中整个生命周期。 接下来一个问题,我并不需要每个属性都在 yaml 文件中去定义啊,有些直接使用官方提供的默认值就可以了。 所以官方又贴心的提供了 additional-spring-configuration-metadata.json 这个 JSON 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
{
"properties": [
{
"sourceType": "org.apache.ibatis.session.Configuration",
"defaultValue": "org.apache.ibatis.scripting.xmltags.XMLLanguageDriver",
"name": "mybatis.configuration.default-scripting-language",
"description": "A default LanguageDriver class.",
"type": "java.lang.Class<? extends org.apache.ibatis.scripting.LanguageDriver>",
"deprecation": {
"reason": "Because when this configuration property is used, there is case that custom language driver cannot be registered correctly.",
"replacement": "mybatis.default-scripting-language-driver"
}
},
{
"sourceType": "org.apache.ibatis.session.Configuration",
"defaultValue": "org.apache.ibatis.type.EnumTypeHandler",
"name": "mybatis.configuration.default-enum-type-handler",
"description": "A default TypeHandler class for Enum.",
"type": "java.lang.Class<? extends org.apache.ibatis.type.TypeHandler>"
},
{
"defaultValue": false,
"name": "mybatis.lazy-initialization",
"description": "Set whether enable lazy initialization for mapper bean.",
"type": "java.lang.Boolean"
},
{
"name": "mybatis.scripting-language-driver.velocity.userdirective",
"deprecation": {
"level": "error",
"reason": "The 'userdirective' is deprecated since Velocity 2.x. This property defined for keeping backward compatibility with older velocity version.",
"replacement": "mybatis.scripting-language-driver.velocity.velocity-settings.runtime.custom_directives"
}
}

]
}

不好理解的话,我们直接看第三个默认配置就好了:

1
2
3
4
5
6
{
"defaultValue": false,
"name": "mybatis.lazy-initialization",
"description": "Set whether enable lazy initialization for mapper bean.",
"type": "java.lang.Boolean"
}

看了下图应该明白了吧,上面那个文件就是定义配置的一些说明、默认值的。

SpringBoot

OK,现代 Java 开发者应该都对 SpringBoot 很熟悉了吧,一个很 "轻量级"IOC 容器。 我记得 SpringBoot 刚出来的时候,很多博客文章都会说 SpringBoot 减轻了 Java 开发工作者的负担,是个轻量级的框架。然而后面我才发现,并不轻量级。因为,SpringBoot 把需要依赖的东西给封装了起来,但其实比起之前自己控制依赖项目来说,反而会更重了一些,毕竟以前还可以自由组合。现在,SpringBoot 以及框架作者都提供了默认的依赖以及默认的配置,所以说这个框架轻量级其实并不是,要说轻量级应该只是说开发轻量级而已,菜鸟也可以快速上手建立一个后台项目而不必去关心太多项目配置的东西。 框架肯定是一个优秀的框架,我们项目全体也都是 SpringBoot 架构起来的,所以还是需要看看,SpringBoot 偷偷帮我们做了什么事情。

依赖简化

日常使用中,我们只需要引入一个 starter,就可以神奇把我们整合的框架整合起来。但是在以前,我们要使用 MyBatis 的时候,却需要引入 org.mybatis:mybatis 主框架,然后还因为需要整合 Spring 框架,所以我们还需要引入一个 org.mybatis:mybatis-spring。这就算了,这两个包如果版本号对不上,还要出现兼容性的问题。 然而现在我们只需要一个 org.mybatis.spring.boot:mybatis-spring-boot-starter,就可以同时引入这两个包,而且版本号还解决的很OK。所以现在我们可以看看他的 pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot</artifactId>
<version>1.3.1</version>
</parent>
<artifactId>mybatis-spring-boot-starter</artifactId>
<name>mybatis-spring-boot-starter</name>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
</dependency>
</dependencies>
</project>

所以结论是,一个 starter,定义了需要依赖的包的版本,然后通过依赖传递将这些包传递到我们 starter 所在的项目上来。 目前我建立了一个项目,这个项目很简单,只是依赖了一个 spring-boot-starter-web, 我们可以在 idea 上很方便的查看依赖的所有东西:

但其实这个做法在以前 Spring 的时候是已经存在了,这个项目就是 Spring-IO,但我也不知道为什么,可能因为名字取得不够好吧?然后这个项目好像很少人用。或者以前的人觉得会被依赖很多东西进来所以不用这个项目了?

自动装配

我们项目所需要的依赖都弄进来了,下一步就是配置让这些包可以相互配合,共同提供服务呀。 所以,SpringBoot 所提供的第二个功能就是,根据默认的配置,装配依赖进来包里面的 Bean 实例。依赖+装配,就是我们以前一直所需要的操作了,可以说这个框架减少了全世界程序员按 CTRL+C/V 的次数hhhhh。 OK,简单了解的话,我需要一个额外的 starter,这里就使用 org.mybatis.spring.boot:mybatis-spring-boot-starter 来看,仓库地址 项目的目录结构为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
├── LICENSE
├── README.md
├── license.txt
├── mvnw
├── mvnw.cmd
├── mybatis-spring-boot-autoconfigure
│   ├── format.xml
│   ├── license.txt
│   ├── pom.xml
│   └── src
├── mybatis-spring-boot-samples
│   ├── mybatis-spring-boot-sample-annotation
│   ├── mybatis-spring-boot-sample-freemarker
│   ├── mybatis-spring-boot-sample-freemarker-legacy
│   ├── mybatis-spring-boot-sample-groovy
│   ├── mybatis-spring-boot-sample-kotlin
│   ├── mybatis-spring-boot-sample-thymeleaf
│   ├── mybatis-spring-boot-sample-velocity
│   ├── mybatis-spring-boot-sample-velocity-legacy
│   ├── mybatis-spring-boot-sample-war
│   ├── mybatis-spring-boot-sample-web
│   ├── mybatis-spring-boot-sample-xml
│   └── pom.xml
├── mybatis-spring-boot-starter
│   ├── license.txt
│   └── pom.xml
├── mybatis-spring-boot-starter-test
│   └── pom.xml
├── mybatis-spring-boot-test-autoconfigure
│   ├── format.xml
│   ├── pom.xml
│   └── src
├── pom.xml
└── travis
├── after_success.sh
└── settings.xml

其中,mybatis-spring-boot-autoconfigure 比较惹人注目,所以我们现在就看看这个项目。 首先我们看看 main/resources/META-INF/spring.factories,因为 Spring 很喜欢通过这些 meta 文件来促使各个模块很好的解耦但又能彼此配合工作,所以这个文件是定义 自动装配 开始的工厂类:

1
2
3
4
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration,\
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration

那么通过看初步的源码,这两个类都是熟悉的 Java 装配类,那么将会被加入到前面提到的 BeanFactory 容器中,后面解析将会调用里面的配置方法。 第二个优秀的地方就是,定义配置类,就可以在 yaml 文件中提示的出现,可以利用 IDE 工具很好的防止配置名写错。这个类就是 MybatisProperties.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@ConfigurationProperties(prefix = MybatisProperties.MYBATIS_PREFIX)
public class MybatisProperties {

public static final String MYBATIS_PREFIX = "mybatis";

private static final ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();

/**
* Location of MyBatis xml config file.
*/
private String configLocation;

/**
* Locations of MyBatis mapper files.
*/
private String[] mapperLocations;

...
}

OK,我们可以看到,定义了前缀就是 mybatis,而这个类是个贫血型的 Bean,只有属性。这些属性将会贯穿 MyBatis 在项目中整个生命周期。 接下来一个问题,我并不需要每个属性都在 yaml 文件中去定义啊,有些直接使用官方提供的默认值就可以了。 所以官方又贴心的提供了 additional-spring-configuration-metadata.json 这个 JSON 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
{
"properties": [
{
"sourceType": "org.apache.ibatis.session.Configuration",
"defaultValue": "org.apache.ibatis.scripting.xmltags.XMLLanguageDriver",
"name": "mybatis.configuration.default-scripting-language",
"description": "A default LanguageDriver class.",
"type": "java.lang.Class<? extends org.apache.ibatis.scripting.LanguageDriver>",
"deprecation": {
"reason": "Because when this configuration property is used, there is case that custom language driver cannot be registered correctly.",
"replacement": "mybatis.default-scripting-language-driver"
}
},
{
"sourceType": "org.apache.ibatis.session.Configuration",
"defaultValue": "org.apache.ibatis.type.EnumTypeHandler",
"name": "mybatis.configuration.default-enum-type-handler",
"description": "A default TypeHandler class for Enum.",
"type": "java.lang.Class<? extends org.apache.ibatis.type.TypeHandler>"
},
{
"defaultValue": false,
"name": "mybatis.lazy-initialization",
"description": "Set whether enable lazy initialization for mapper bean.",
"type": "java.lang.Boolean"
},
{
"name": "mybatis.scripting-language-driver.velocity.userdirective",
"deprecation": {
"level": "error",
"reason": "The 'userdirective' is deprecated since Velocity 2.x. This property defined for keeping backward compatibility with older velocity version.",
"replacement": "mybatis.scripting-language-driver.velocity.velocity-settings.runtime.custom_directives"
}
}

]
}

不好理解的话,我们直接看第三个默认配置就好了:

1
2
3
4
5
6
{
"defaultValue": false,
"name": "mybatis.lazy-initialization",
"description": "Set whether enable lazy initialization for mapper bean.",
"type": "java.lang.Boolean"
}

看了下图应该明白了吧,上面那个文件就是定义配置的一些说明、默认值的。

所以当我们要造一个框架,又因为很多约定的东西,靠人脑来记已经靠不住的情况下,就可以编写类似于 additional-spring-configuration-metadata.json 这种文件来做约定以及说明了。


OK,上面说完了 SpringBoot 的两个最主要的优点以后,现在就来看看源码。但是源码这块,因为 传递依赖 利用的是 mvn/gradle 的特性,所以 依赖传递 并不需要再说。 那么最主要的就是来解析,怎么自动装配的问题。

简单的SpringBoot项目

pom.xml 依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.BUILD-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.liweidan.web</groupId>
<artifactId>web-test</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>web-test</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

一个主启动器:

1
2
3
4
5
6
7
8
@SpringBootApplication
public class WebTestApplication {

public static void main(String[] args) {
SpringApplication.run(WebTestApplication.class, args);
}

}

一个控制器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
public class UserController {

@GetMapping("users")
public List<Map<String, String>> users() {
List<Map<String, String>> users = new ArrayList<>();
for (int i = 0; i < 3; i++) {
Map<String, String> u = new HashMap<>();
u.put("id", String.valueOf(i));
u.put("name", "name" + i);
users.add(u);
}
return users;
}

}

SpringApplication元信息准备

写过 SpringBoot 项目的同学应该对这个很熟悉了,通过传递一个上下文的根类,SpringBoot 将会自动装载在此类所在的包下面的所有类,并且 args 很明显就是我们在控制台传递的参数,也一并传递给 SpringApplication.run 这个方法,SpringBoot 即可将命令行的参数配置覆盖配置文件指定的配置。 接下来看看这个类做了什么事情:

1
2
3
4
5
6
7
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
return run(new Class<?>[] { primarySource }, args);
}

public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
return new SpringApplication(primarySources).run(args);
}

通过静态方法,传递一个 Class 类作为主资源,然后再传递给主资源数组的 run 方法。那么这个数组其实我们是可以传递多个主资源的,比如我们做项目的时候,想要每个模块包彼此分离,即可传递多个包的主资源路径。 new SpringApplication(primarySources).run(args); 才是真正的进入容器的准备阶段: 首先看看构造器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
// 初始化主资源链表,用于下面需要读取的时候可以遍历.
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
// 判断Web环境,有webFlux/web/普通三个环境,主要通过类路径是否带有相对应需要的类来判断,如果都没有则初始化为普通Java项目.
this.webApplicationType = WebApplicationType.deduceFromClasspath();
// 初始化环境,这个接口多用于web环境,因为需要从web上下文加载一些信息.
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
// 初始化监听器,监听容器生命周期中需要回调的函数
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
// 从运行堆栈中寻找运行这个类的main方法所在的类
this.mainApplicationClass = deduceMainApplicationClass();
}

加载不同模块的元信息

在上面的构造器中看到 setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));Spring 还是跟以前获取 handlers 一样。通过在 META-INF 类加载路径定义不同的 spring.factories,使用类加载器读取这些配置文件资源,解析,加载配置文件中的类,初始化对象,来共同完成业务。

1
2
3
4
5
6
7
8
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
ClassLoader classLoader = getClassLoader();
// Use names and ensure unique to protect against duplicates
Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
AnnotationAwareOrderComparator.sort(instances);
return instances;
}

通过 SpringFactoriesLoader.loadFactoryNames(type, classLoader),加载当前 ClassLoader 中的指定类,源码显示如何读取,后面的 getOrDefault(factoryTypeName, Collections.emptyList()) 还是比较好理解的,如果加载到工厂类的名字就返回不然返回个空集合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
String factoryTypeName = factoryType.getName();
return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
}

private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
// 相当于Map<String, List<String>>结构,先从缓存命中.
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null) {
return result;
}

/*
属性定义了元信息的路径以及名字
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
*/
try {
// 开始加载类路径下特定名字的元信息文件
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
result = new LinkedMultiValueMap<>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
// 通过UrlResource解析成Properties对象
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
// 遍历所有模块的指定文件,然后加入缓存
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryTypeName = ((String) entry.getKey()).trim();
for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
result.add(factoryTypeName, factoryImplementationName.trim());
}
}
}
cache.put(classLoader, result);
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}

启动项目SpringApplication.run

可以看到 run 的源码跟之前的 AbstractApplicationContext#refresh 的味道还是一样的,先加载一系列容器运行时需要的生命周期类(Spring 模块间的也有用户自定义的),然后 refreshContext(context) 刷新容器上下文。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public ConfigurableApplicationContext run(String... args) {
// 这是用来打印加载时间的工具类,暂时可以略过.
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
// 主要设置JVM虚拟机支持无设备情况下让awt可运行的属性
configureHeadlessProperty();
// 一. 加载所有SpringApplicationRunListeners并开始遍历所有Lintener启动监听回调函数
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// 二. 开始准备ConfigurableEnvironment环境
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
// 三. 创建应用上下文
context = createApplicationContext();
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
// 四. 做一些准备工作
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
// 五. 刷新上下文
refreshContext(context);
// 六. 刷新完成后做的一些清理、回调工作
afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
// 七. 启动完成后,调用所有SpringApplicationRunListener的完成启动的回调函数
listeners.started(context);
// 八. 主要处理ApplicationRunner和CommandLineRunner的回调
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}

try {
// 九. 运行时的SpringApplicationRunListener回调函数
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
return context;
}

装载SpringApplicationRunListener

1
2
3
4
5
private SpringApplicationRunListeners getRunListeners(String[] args) {
Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
return new SpringApplicationRunListeners(logger,
getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args));
}

首先我们看第一步,加载所有的 SpringApplicationRunListener 子类,这个加载方式跟上面所说的加载元信息是一样的,只不过指定加载 SpringApplicationRunListener.class 类。 我们知道,Spring 经常会定义很多生命周期回调,供用户根据需求切入框架。这次 SpringBoot 的生命周期是 SpringApplicationRunListener。 首先看看 SpringApplicationRunListener 定义哪些生命周期函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public interface SpringApplicationRunListener {

// 项目开始时调用
default void starting() {
}

// 环境准备好时
default void environmentPrepared(ConfigurableEnvironment environment) {
}

// 上下文准备好时
default void contextPrepared(ConfigurableApplicationContext context) {
}

// 上下文读取完成
default void contextLoaded(ConfigurableApplicationContext context) {
}

// 启动完成
default void started(ConfigurableApplicationContext context) {
}

// 项目运行时调用
default void running(ConfigurableApplicationContext context) {
}

// 项目失败时
default void failed(ConfigurableApplicationContext context, Throwable exception) {
}

}

准备ConfigurableEnvironment环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments) {
// 首先根据环境类型获取对象的配置实现对象
ConfigurableEnvironment environment = getOrCreateEnvironment();
// 配置ConversionService单例对象,以及判断有没有设置Profiles环境
configureEnvironment(environment, applicationArguments.getSourceArgs());
// 读取其他环境中(比如web.xml)读取的配置属性
ConfigurationPropertySources.attach(environment);
// 继续上面的SpringApplicationRunListener生命周期调用
listeners.environmentPrepared(environment);
// 把环境绑定到Binder中,Binder是Spring提供的一个记录对象的容器
bindToSpringApplication(environment);
if (!this.isCustomEnvironment) {
// 如果当前的配置环境和重新判断的环境不同,则转换成当前的环境(有可能在生命周期修改了加载的环境?)
environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
deduceEnvironmentClass());
}
// 重新读取特定环境的配置
ConfigurationPropertySources.attach(environment);
return environment;
}

private ConfigurableEnvironment getOrCreateEnvironment() {
if (this.environment != null) {
return this.environment;
}
switch (this.webApplicationType) {
case SERVLET:
return new StandardServletEnvironment();
case REACTIVE:
return new StandardReactiveWebEnvironment();
default:
return new StandardEnvironment();
}
}

然后在主线程中配置忽略 BooleanBean

1
2
3
4
5
6
7
8
9
10
11
12
13
public ConfigurableApplicationContext run(String... args) {
//...
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);
//...
}

private void configureIgnoreBeanInfo(ConfigurableEnvironment environment) {
if (System.getProperty(CachedIntrospectionResults.IGNORE_BEANINFO_PROPERTY_NAME) == null) {
Boolean ignore = environment.getProperty("spring.beaninfo.ignore", Boolean.class, Boolean.TRUE);
System.setProperty(CachedIntrospectionResults.IGNORE_BEANINFO_PROPERTY_NAME, ignore.toString());
}
}

创建上下文

1
2
3
4
context = createApplicationContext();
// 获取模块的错误解释器,用于项目启动的时候解析是什么错误
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);

根据不同环境创建不同的上下文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
protected ConfigurableApplicationContext createApplicationContext() {
Class<?> contextClass = this.applicationContextClass;
if (contextClass == null) {
try {
switch (this.webApplicationType) {
case SERVLET:
contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);
break;
case REACTIVE:
contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
break;
default:
contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
}
}
catch (ClassNotFoundException ex) {
throw new IllegalStateException(
"Unable create a default ApplicationContext, please specify an ApplicationContextClass", ex);
}
}
return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
}

Context准备工作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment,
SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
// 设置上面准备好的环境信息
context.setEnvironment(environment);
// 处理之前做一些事情,但是当前环境下,只是注册了一个 ConversionService 到 BeanFactory 中
postProcessApplicationContext(context);
// 调用之前注册的所有Initializers
applyInitializers(context);
// 再调用所有的listeners
listeners.contextPrepared(context);
if (this.logStartupInfo) {
logStartupInfo(context.getParent() == null);
logStartupProfileInfo(context);
}
// 注册SpringBoot独有的一些Bean
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
if (printedBanner != null) {
beanFactory.registerSingleton("springBootBanner", printedBanner);
}
// 接下来跟之前一样
if (beanFactory instanceof DefaultListableBeanFactory) {
((DefaultListableBeanFactory) beanFactory)
.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
}
if (this.lazyInitialization) {
context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
}
// Load the sources
Set<Object> sources = getAllSources();
Assert.notEmpty(sources, "Sources must not be empty");
// 根据我们传递的主类进行读取,放入BeanFactory中
load(context, sources.toArray(new Object[0]));
listeners.contextLoaded(context);
}

其实当前 postProcessApplicationContext 除了注册 ConversionService 以外其他事情都没做。

refreshContext刷新上下文

因为我要看看自动装配的问题,所以这个时候为了故事比较好说,我加了 mybatis-spring-boot-starter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.BUILD-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.liweidan.web</groupId>
<artifactId>web-test</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>web-test</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.197</version>
</dependency>

</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

刷新上下文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void refreshContext(ConfigurableApplicationContext context) {
refresh(context);
if (this.registerShutdownHook) {
try {
context.registerShutdownHook();
}
catch (AccessControlException ex) {
// Not allowed in some environments.
}
}
}
protected void refresh(ApplicationContext applicationContext) {
Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext);
((AbstractApplicationContext) applicationContext).refresh();
}

然后接下来就去到我们熟悉的 AbstractApplicationContext 中,重温一下这个 refresh() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
prepareRefresh();

// Tell the subclass to refresh the internal bean factory.
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);

try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);

// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);

// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);

// Initialize message source for this context.
initMessageSource();

// Initialize event multicaster for this context.
initApplicationEventMulticaster();

// Initialize other special beans in specific context subclasses.
onRefresh();

// Check for listener beans and register them.
registerListeners();

// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);

// Last step: publish corresponding event.
finishRefresh();
}

catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}

// Destroy already created singletons to avoid dangling resources.
destroyBeans();

// Reset 'active' flag.
cancelRefresh(ex);

// Propagate exception to caller.
throw ex;
}

finally {
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
resetCommonCaches();
}
}
}

同之前一样,在 invokeBeanFactoryPostProcessors(beanFactory) 这句话出现了 BeanDefinition 数量的剧增,那么我们可以推断出,这句话还是关键,就是在这句话开始解析我们的项目依赖。

其实按照我的猜测,自动配置应该是 starter 提供了一些配置类交给 SpringBoot 注入 Spring 的 BeanFactory 中,但是现在看来,好像 SpringBoot 就不需要做什么东西,直接委托给容器扫描解析执行了。

invokeBeanFactoryPostProcessors

1
2
3
4
5
6
7
8
9
10
11
protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
// 利用委派模式委托给 PostProcessorRegistrationDelegate 进行执行
PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());

// Detect a LoadTimeWeaver and prepare for weaving, if found in the meantime
// (e.g. through an @Bean method registered by ConfigurationClassPostProcessor)
if (beanFactory.getTempClassLoader() == null && beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) {
beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));
beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
}
}

PostProcessorRegistrationDelegate委派对象

这是一个在 Context 模块下对象,主要处理 PostProcessor 相关的事情,看到签名只提供两个方法:

  • invokeBeanFactoryPostProcessors 执行 BF 的 PostProcessors
  • registerBeanPostProcessors 注册 PostProcessors 到 BF 中

关于普通的配置类解析,之前在 chapters3_高级的Beafctory—Spring上下文 已经有提到,解析项目中配置类不明白的话可以先看看那篇文章。 不过我们现在重点是怎么读取到 starter 框架里边的自动配置信息:

1
2
3
4
5
6
7
private static void invokeBeanDefinitionRegistryPostProcessors(
Collection<? extends BeanDefinitionRegistryPostProcessor> postProcessors, BeanDefinitionRegistry registry) {

for (BeanDefinitionRegistryPostProcessor postProcessor : postProcessors) {
postProcessor.postProcessBeanDefinitionRegistry(registry);
}
}

当前 postProcessors 里边只有一个 ConfigurationClassPostProcessor 那我们进入对应的 postProcessBeanDefinitionRegistry 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
int registryId = System.identityHashCode(registry);
if (this.registriesPostProcessed.contains(registryId)) {
throw new IllegalStateException(
"postProcessBeanDefinitionRegistry already called on this post-processor against " + registry);
}
if (this.factoriesPostProcessed.contains(registryId)) {
throw new IllegalStateException(
"postProcessBeanFactory already called on this post-processor against " + registry);
}
this.registriesPostProcessed.add(registryId);

processConfigBeanDefinitions(registry);
}

processConfigBeanDefinitions 解析配置

由于我们现在需要跟自动导入的配置,所以我们应该需要跟的类配置是我们的主类,也就是 WebTestApplication 这个类,这个类上修饰的注解 @SpringBootApplication 是自动注入的关键

这个才是执行自动配置的重点之重:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
/**
* Build and validate a configuration model based on the registry of
* {@link Configuration} classes.
*/
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
List<BeanDefinitionHolder> configCandidates = new ArrayList<>();
String[] candidateNames = registry.getBeanDefinitionNames();

for (String beanName : candidateNames) {
BeanDefinition beanDef = registry.getBeanDefinition(beanName);
if (beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE) != null) {
if (logger.isDebugEnabled()) {
logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);
}
}
else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
}
}

// Return immediately if no @Configuration classes were found
if (configCandidates.isEmpty()) {
return;
}

// Sort by previously determined @Order value, if applicable
configCandidates.sort((bd1, bd2) -> {
int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition());
int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition());
return Integer.compare(i1, i2);
});

// Detect any custom bean name generation strategy supplied through the enclosing application context
SingletonBeanRegistry sbr = null;
if (registry instanceof SingletonBeanRegistry) {
sbr = (SingletonBeanRegistry) registry;
if (!this.localBeanNameGeneratorSet) {
BeanNameGenerator generator = (BeanNameGenerator) sbr.getSingleton(
AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR);
if (generator != null) {
this.componentScanBeanNameGenerator = generator;
this.importBeanNameGenerator = generator;
}
}
}

if (this.environment == null) {
this.environment = new StandardEnvironment();
}

// Parse each @Configuration class
ConfigurationClassParser parser = new ConfigurationClassParser(
this.metadataReaderFactory, this.problemReporter, this.environment,
this.resourceLoader, this.componentScanBeanNameGenerator, registry);

Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());
// ↑ 这上面属于解析开始的准备工作 ↑
do {
parser.parse(candidates);
parser.validate();

Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
configClasses.removeAll(alreadyParsed);

// Read the model and create bean definitions based on its content
if (this.reader == null) {
this.reader = new ConfigurationClassBeanDefinitionReader(
registry, this.sourceExtractor, this.resourceLoader, this.environment,
this.importBeanNameGenerator, parser.getImportRegistry());
}
this.reader.loadBeanDefinitions(configClasses);
alreadyParsed.addAll(configClasses);

candidates.clear();
if (registry.getBeanDefinitionCount() > candidateNames.length) {
String[] newCandidateNames = registry.getBeanDefinitionNames();
Set<String> oldCandidateNames = new HashSet<>(Arrays.asList(candidateNames));
Set<String> alreadyParsedClasses = new HashSet<>();
for (ConfigurationClass configurationClass : alreadyParsed) {
alreadyParsedClasses.add(configurationClass.getMetadata().getClassName());
}
for (String candidateName : newCandidateNames) {
if (!oldCandidateNames.contains(candidateName)) {
BeanDefinition bd = registry.getBeanDefinition(candidateName);
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, this.metadataReaderFactory) &&
!alreadyParsedClasses.contains(bd.getBeanClassName())) {
candidates.add(new BeanDefinitionHolder(bd, candidateName));
}
}
}
candidateNames = newCandidateNames;
}
}
while (!candidates.isEmpty());

// Register the ImportRegistry as a bean in order to support ImportAware @Configuration classes
if (sbr != null && !sbr.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) {
sbr.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry());
}

if (this.metadataReaderFactory instanceof CachingMetadataReaderFactory) {
// Clear cache in externally provided MetadataReaderFactory; this is a no-op
// for a shared cache since it'll be cleared by the ApplicationContext.
((CachingMetadataReaderFactory) this.metadataReaderFactory).clearCache();
}
}

重点看看 parser.parse(candidates); 这句话是怎么解析的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public void parse(Set<BeanDefinitionHolder> configCandidates) {
for (BeanDefinitionHolder holder : configCandidates) {
BeanDefinition bd = holder.getBeanDefinition();
try {
if (bd instanceof AnnotatedBeanDefinition) {
// 因为这是一个使用注解的Bean,所以应该关注这里
parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
}
else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) {
parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName());
}
else {
parse(bd.getBeanClassName(), holder.getBeanName());
}
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex);
}
}

this.deferredImportSelectorHandler.process();
}

protected final void parse(AnnotationMetadata metadata, String beanName) throws IOException {
processConfigurationClass(new ConfigurationClass(metadata, beanName));
}

protected void processConfigurationClass(ConfigurationClass configClass) throws IOException {
// 如果当前的类是一个解析类,则跳过解析
if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {
return;
}

// 判断是否已经解析过了
ConfigurationClass existingClass = this.configurationClasses.get(configClass);
if (existingClass != null) {
if (configClass.isImported()) {
if (existingClass.isImported()) {
existingClass.mergeImportedBy(configClass);
}
// Otherwise ignore new imported config class; existing non-imported class overrides it.
return;
}
else {
// Explicit bean definition found, probably replacing an import.
// Let's remove the old one and go with the new one.
this.configurationClasses.remove(configClass);
this.knownSuperclasses.values().removeIf(configClass::equals);
}
}

// 递归解析配置类以及他的父类、接口等等
SourceClass sourceClass = asSourceClass(configClass);
do {
sourceClass = doProcessConfigurationClass(configClass, sourceClass);
}
while (sourceClass != null);

this.configurationClasses.put(configClass, configClass);
}

然后我们要进入 doProcessConfigurationClass 观察解析的过程。 这串代码其实有点长……:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
@Nullable
protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)
throws IOException {

if (configClass.getMetadata().isAnnotated(Component.class.getName())) {
// Recursively process any member (nested) classes first
processMemberClasses(configClass, sourceClass);
}

// 解析需要导入Property配置文件的类
for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), PropertySources.class,
org.springframework.context.annotation.PropertySource.class)) {
if (this.environment instanceof ConfigurableEnvironment) {
processPropertySource(propertySource);
}
else {
logger.info("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +
"]. Reason: Environment must implement ConfigurableEnvironment");
}
}

// 解析 @ComponentScan
Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
if (!componentScans.isEmpty() &&
!this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
for (AnnotationAttributes componentScan : componentScans) {
// The config class is annotated with @ComponentScan -> perform the scan immediately
Set<BeanDefinitionHolder> scannedBeanDefinitions =
this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
// Check the set of scanned definitions for any further config classes and parse recursively if needed
for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
if (bdCand == null) {
bdCand = holder.getBeanDefinition();
}
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
parse(bdCand.getBeanClassName(), holder.getBeanName());
}
}
}
}

// 解析 @Import 的类,这句话是重点
processImports(configClass, sourceClass, getImports(sourceClass), true);

// Process any @ImportResource annotations
AnnotationAttributes importResource =
AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class);
if (importResource != null) {
String[] resources = importResource.getStringArray("locations");
Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader");
for (String resource : resources) {
String resolvedResource = this.environment.resolveRequiredPlaceholders(resource);
configClass.addImportedResource(resolvedResource, readerClass);
}
}

// 解析 @Bean 方法
Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);
for (MethodMetadata methodMetadata : beanMethods) {
configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
}

// 解析接口默认方法
processInterfaces(configClass, sourceClass);

// 如果有父类,返回父类,由上一层继续调用这个方法继续解析
if (sourceClass.getMetadata().hasSuperClass()) {
String superclass = sourceClass.getMetadata().getSuperClassName();
if (superclass != null && !superclass.startsWith("java") &&
!this.knownSuperclasses.containsKey(superclass)) {
this.knownSuperclasses.put(superclass, configClass);
// Superclass found, return its annotation metadata and recurse
return sourceClass.getSuperClass();
}
}

// 返回Null表示解析结束
return null;
}

好了,可以看到通过 Java 类的解析可谓覆盖到尼玛我想象不到的地方….但他们就是应该要有 挑重点来看吧,我的主类没有父级也没有接口,直接重点看 processImports(configClass, sourceClass, getImports(sourceClass), true) 这句话。 这句话首先需要获取所有的导入配置 getImports(sourceClass)

1
2
3
4
5
6
private Set<SourceClass> getImports(SourceClass sourceClass) throws IOException {
Set<SourceClass> imports = new LinkedHashSet<>();
Set<SourceClass> visited = new LinkedHashSet<>();
collectImports(sourceClass, imports, visited);
return imports;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
private void collectImports(SourceClass sourceClass, Set<SourceClass> imports, Set<SourceClass> visited)
throws IOException {

if (visited.add(sourceClass)) {
for (SourceClass annotation : sourceClass.getAnnotations()) {
String annName = annotation.getMetadata().getClassName();
if (!annName.equals(Import.class.getName())) {
collectImports(annotation, imports, visited);
}
}
imports.addAll(sourceClass.getAnnotationAttributes(Import.class.getName(), "value"));
}
}

这两个东西,切忌不要去 debug 一个一个看….我在这里看晕了好几个小时,应该是条件断点判断 sourceClass.getAnnotationAttributes(Import.class.getName(), "value").size() > 0 再停住

好了等到断点停住的时候,发现是 @interface SpringBootApplication > @interface EnableAutoConfiguration 上面的 @Import(AutoConfigurationImportSelector.class) 在这里导入了。以及这个类上边的 @AutoConfigurationPackage 导入了 @Import(AutoConfigurationPackages.Registrar.class) 所以当前需要解析的 importCandidatesAutoConfigurationPackages.Registrar.class 以及 AutoConfigurationImportSelector.class 两个类。 那么这两个类是什么用的,我现在也还不知道…先知道他被导入就好了,接下去看 processImports 方法了,这个方法应该有答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
Collection<SourceClass> importCandidates, boolean checkForCircularImports) {

if (importCandidates.isEmpty()) {
return;
}

// 由上面传递的 checkForCircularImports 参数决定是否判断有没有循环引入
if (checkForCircularImports && isChainedImportOnStack(configClass)) {
this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
}
else {
this.importStack.push(configClass);
try {
// 开始解析导入类
for (SourceClass candidate : importCandidates) {
// 第一层,根据不同的导入类型进行解析
// AutoConfigurationImportSelector 来到这个分支
if (candidate.isAssignable(ImportSelector.class)) {
// Candidate class is an ImportSelector -> delegate to it to determine imports
Class<?> candidateClass = candidate.loadClass();
ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class,
this.environment, this.resourceLoader, this.registry);
if (selector instanceof DeferredImportSelector) {
this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
}
else {
String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames);
// 递归解析
processImports(configClass, currentSourceClass, importSourceClasses, false);
}
}
// AutoConfigurationPackages.Registrar进入这个分支
else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
// Candidate class is an ImportBeanDefinitionRegistrar ->
// delegate to it to register additional bean definitions
Class<?> candidateClass = candidate.loadClass();
ImportBeanDefinitionRegistrar registrar =
ParserStrategyUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class,
this.environment, this.resourceLoader, this.registry);
configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
}
else {
// 直接以@Configuration的方式进行处理
this.importStack.registerImport(
currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
processConfigurationClass(candidate.asConfigClass(configClass));
}
}
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to process import candidates for configuration class [" +
configClass.getMetadata().getClassName() + "]", ex);
}
finally {
this.importStack.pop();
}
}
}

不行呀,因为需要根据类型来做,先给两个类的类型吧:

这三个分支对应着三个不同的配置类型:

  1. ImportSelector 用于导入配置类的信息
  2. Registrar 用于解析自定义注解动态生成 Bean 的信息
  3. @Configuration 读取到配置类,直接解析

那我们就先按照上图的顺序来解析这些配置类吧,首先是 AutoConfigurationPackages.Registrar 对象的解析。我先从上面一大串截取代码片段来看:

1
2
3
4
5
6
7
8
9
10
11
// ...
else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
// 额外注册BeanDefinition用
Class<?> candidateClass = candidate.loadClass();
ImportBeanDefinitionRegistrar registrar =
ParserStrategyUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class,
this.environment, this.resourceLoader, this.registry);
// 简单的添加(导入)到当前的 configClass 中
configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
}
// ...

先放到后面,应该是解析的时候会调用到。 接下来看第二个 AutoConfigurationImportSelector.class 的解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (candidate.isAssignable(ImportSelector.class)) {
// Candidate class is an ImportSelector -> delegate to it to determine imports
Class<?> candidateClass = candidate.loadClass();
ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class,
this.environment, this.resourceLoader, this.registry);
if (selector instanceof DeferredImportSelector) {
// 进入这里
this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
}
else {
String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames);
processImports(configClass, currentSourceClass, importSourceClasses, false);
}
}

这一块比较麻烦,在上面 uml 可以看到,AutoConfigurationImportSelector.class 属于 DeferredImportSelector,所以交给 deferredImportSelectorHandler 进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
public void handle(ConfigurationClass configClass, DeferredImportSelector importSelector) {
DeferredImportSelectorHolder holder = new DeferredImportSelectorHolder(
configClass, importSelector);
if (this.deferredImportSelectors == null) {
DeferredImportSelectorGroupingHandler handler = new DeferredImportSelectorGroupingHandler();
handler.register(holder);
handler.processGroupImports();
}
else {
// 来到这里,简单的加入
this.deferredImportSelectors.add(holder);
}
}

导入刚刚注册的配置

上面一大堆全是解析,然后接下来就要处理这些自动配置了。 回到 parser.parse(candidates); 这个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public void parse(Set<BeanDefinitionHolder> configCandidates) {
for (BeanDefinitionHolder holder : configCandidates) {
BeanDefinition bd = holder.getBeanDefinition();
try {
if (bd instanceof AnnotatedBeanDefinition) {
parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
}
else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) {
parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName());
}
else {
parse(bd.getBeanClassName(), holder.getBeanName());
}
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex);
}
}

// 处理导入配置类(第三方配置)
this.deferredImportSelectorHandler.process();
}

AutoConfigurationImportSelector解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void process() {
// AutoConfigurationImportSelector集合 只有一个元素
List<DeferredImportSelectorHolder> deferredImports = this.deferredImportSelectors;
this.deferredImportSelectors = null;
try {
if (deferredImports != null) {
DeferredImportSelectorGroupingHandler handler = new DeferredImportSelectorGroupingHandler();
deferredImports.sort(DEFERRED_IMPORT_COMPARATOR);
// 注册到 DeferredImportSelectorGroupingHandler 中
deferredImports.forEach(handler::register);
// 重点看这边
handler.processGroupImports();
}
}
finally {
this.deferredImportSelectors = new ArrayList<>();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void processGroupImports() {
for (DeferredImportSelectorGrouping grouping : this.groupings.values()) {
grouping.getImports().forEach(entry -> {
ConfigurationClass configurationClass = this.configurationClasses.get(
entry.getMetadata());
try {
processImports(configurationClass, asSourceClass(configurationClass),
asSourceClasses(entry.getImportClassName()), false);
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to process import candidates for configuration class [" +
configurationClass.getMetadata().getClassName() + "]", ex);
}
});
}
}

先进入这里 grouping.getImports()

1
2
3
4
5
6
7
public Iterable<Group.Entry> getImports() {
for (DeferredImportSelectorHolder deferredImport : this.deferredImports) {
this.group.process(deferredImport.getConfigurationClass().getMetadata(),
deferredImport.getImportSelector());
}
return this.group.selectImports();
}

这个过程就是 SpringBoot 提供的,在类路径提供 additional-spring-configuration-metadata.json 提供导入配置的,然而看起来好像所有的 SpringBoot 模块都在这里了,他只是通过导入类路径的包来判断是否要加载配置。 跳过这一段吧,看着有点累。我们现在只要知道,读取了类路径下的 additional-spring-configuration-metadata.json 后,我们想要看到的 MybatisAutoConfiguration 已经被读取到了就好了。

同样的,spring-boot-mybatis-starter 也提供了上面的注册文件:

好了,接下来回到上面的解析方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void processGroupImports() {
for (DeferredImportSelectorGrouping grouping : this.groupings.values()) {
grouping.getImports().forEach(entry -> {
ConfigurationClass configurationClass = this.configurationClasses.get(
entry.getMetadata());
try {
// 此时进入processImports的时候就不需要再导入配置了
// 直接以 @Configuration 去实现配置
processImports(configurationClass, asSourceClass(configurationClass),
asSourceClasses(entry.getImportClassName()), false);
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to process import candidates for configuration class [" +
configurationClass.getMetadata().getClassName() + "]", ex);
}
});
}
}

那么关于 @Configuration 的解析,在我 之前的文章 就可以看到了。 现在就看看怎么根据主类的所在的包,扫描该包下的所有 Bean

自动包扫描注册Bean

我们得回到 doProcessConfigurationClass 的这个地方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 解析 @ComponentScan,会从主类 WebTestApplication 中拿到 BasePackage
// 然后根据这个 BasePackage 的配置进行扫描下面的类
Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
if (!componentScans.isEmpty() &&
!this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
for (AnnotationAttributes componentScan : componentScans) {
// 通过 componentScanParser 进行解析
Set<BeanDefinitionHolder> scannedBeanDefinitions =
this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
// Check the set of scanned definitions for any further config classes and parse recursively if needed
for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
if (bdCand == null) {
bdCand = holder.getBeanDefinition();
}
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
parse(bdCand.getBeanClassName(), holder.getBeanName());
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, final String declaringClass) {
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(this.registry,
componentScan.getBoolean("useDefaultFilters"), this.environment, this.resourceLoader);

Class<? extends BeanNameGenerator> generatorClass = componentScan.getClass("nameGenerator");
boolean useInheritedGenerator = (BeanNameGenerator.class == generatorClass);
scanner.setBeanNameGenerator(useInheritedGenerator ? this.beanNameGenerator :
BeanUtils.instantiateClass(generatorClass));

ScopedProxyMode scopedProxyMode = componentScan.getEnum("scopedProxy");
if (scopedProxyMode != ScopedProxyMode.DEFAULT) {
scanner.setScopedProxyMode(scopedProxyMode);
}
else {
Class<? extends ScopeMetadataResolver> resolverClass = componentScan.getClass("scopeResolver");
scanner.setScopeMetadataResolver(BeanUtils.instantiateClass(resolverClass));
}

scanner.setResourcePattern(componentScan.getString("resourcePattern"));

for (AnnotationAttributes filter : componentScan.getAnnotationArray("includeFilters")) {
for (TypeFilter typeFilter : typeFiltersFor(filter)) {
scanner.addIncludeFilter(typeFilter);
}
}
for (AnnotationAttributes filter : componentScan.getAnnotationArray("excludeFilters")) {
for (TypeFilter typeFilter : typeFiltersFor(filter)) {
scanner.addExcludeFilter(typeFilter);
}
}

boolean lazyInit = componentScan.getBoolean("lazyInit");
if (lazyInit) {
scanner.getBeanDefinitionDefaults().setLazyInit(true);
}

Set<String> basePackages = new LinkedHashSet<>();
String[] basePackagesArray = componentScan.getStringArray("basePackages");
for (String pkg : basePackagesArray) {
String[] tokenized = StringUtils.tokenizeToStringArray(this.environment.resolvePlaceholders(pkg),
ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
Collections.addAll(basePackages, tokenized);
}
for (Class<?> clazz : componentScan.getClassArray("basePackageClasses")) {
basePackages.add(ClassUtils.getPackageName(clazz));
}
if (basePackages.isEmpty()) {
// 没有指定扫描包,进入这里获取主类的包名
basePackages.add(ClassUtils.getPackageName(declaringClass));
}

scanner.addExcludeFilter(new AbstractTypeHierarchyTraversingFilter(false, false) {
@Override
protected boolean matchClassName(String className) {
return declaringClass.equals(className);
}
});

// 上面基本上都是解析配置的一些东西,过滤器啊,LazyInit等等,到这一步才是真正的扫描
// StringUtils.toStringArray(basePackages) 获取包名
return scanner.doScan(StringUtils.toStringArray(basePackages));
}

真正的扫描,做的事情可谓多得多

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
Assert.notEmpty(basePackages, "At least one base package must be specified");
Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
for (String basePackage : basePackages) {
// 这里又是一个大的模块了,也就是读取ClassPath下所有的文件
Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
// 拿到扫描到的 BeanDefinition 注册到工厂中
for (BeanDefinition candidate : candidates) {
ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
candidate.setScope(scopeMetadata.getScopeName());
String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
if (candidate instanceof AbstractBeanDefinition) {
postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
}
if (candidate instanceof AnnotatedBeanDefinition) {
AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
}
if (checkCandidate(beanName, candidate)) {
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
definitionHolder =
AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
beanDefinitions.add(definitionHolder);
registerBeanDefinition(definitionHolder, this.registry);
}
}
}
return beanDefinitions;
}

扫描:

1
2
3
4
5
6
7
8
public Set<BeanDefinition> findCandidateComponents(String basePackage) {
if (this.componentsIndex != null && indexSupportsIncludeFilters()) {
return addCandidateComponentsFromIndex(this.componentsIndex, basePackage);
}
else {
return scanCandidateComponents(basePackage);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
Set<BeanDefinition> candidates = new LinkedHashSet<>();
try {
String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
resolveBasePackage(basePackage) + '/' + this.resourcePattern;
Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
boolean traceEnabled = logger.isTraceEnabled();
boolean debugEnabled = logger.isDebugEnabled();
// 拿到包下两个文件的 Resource 实例
for (Resource resource : resources) {
if (traceEnabled) {
logger.trace("Scanning " + resource);
}
if (resource.isReadable()) {
try {
MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
if (isCandidateComponent(metadataReader)) {
ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
sbd.setResource(resource);
sbd.setSource(resource);
// 符合某种条件,暂且就当BF中还没存在这个Bean吧
if (isCandidateComponent(sbd)) {
if (debugEnabled) {
logger.debug("Identified candidate component class: " + resource);
}
// 添加
candidates.add(sbd);
}
else {
if (debugEnabled) {
logger.debug("Ignored because not a concrete top-level class: " + resource);
}
}
}
else {
if (traceEnabled) {
logger.trace("Ignored because not matching any filter: " + resource);
}
}
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to read candidate component class: " + resource, ex);
}
}
else {
if (traceEnabled) {
logger.trace("Ignored because not readable: " + resource);
}
}
}
}
catch (IOException ex) {
throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex);
}
// 返回
return candidates;
}

OK,扫描完成,后在 finishBeanFactoryInitialization 进行初始化就可以了

结束

所以当我们要造一个框架,又因为很多约定的东西,靠人脑来记已经靠不住的情况下,就可以编写类似于 additional-spring-configuration-metadata.json 这种文件来做约定以及说明了。


OK,上面说完了 SpringBoot 的两个最主要的优点以后,现在就来看看源码。但是源码这块,因为 传递依赖 利用的是 mvn/gradle 的特性,所以 依赖传递 并不需要再说。 那么最主要的就是来解析,怎么自动装配的问题。

简单的SpringBoot项目

pom.xml 依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.BUILD-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.liweidan.web</groupId>
<artifactId>web-test</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>web-test</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

一个主启动器:

1
2
3
4
5
6
7
8
@SpringBootApplication
public class WebTestApplication {

public static void main(String[] args) {
SpringApplication.run(WebTestApplication.class, args);
}

}

一个控制器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
public class UserController {

@GetMapping("users")
public List<Map<String, String>> users() {
List<Map<String, String>> users = new ArrayList<>();
for (int i = 0; i < 3; i++) {
Map<String, String> u = new HashMap<>();
u.put("id", String.valueOf(i));
u.put("name", "name" + i);
users.add(u);
}
return users;
}

}

SpringApplication元信息准备

写过 SpringBoot 项目的同学应该对这个很熟悉了,通过传递一个上下文的根类,SpringBoot 将会自动装载在此类所在的包下面的所有类,并且 args 很明显就是我们在控制台传递的参数,也一并传递给 SpringApplication.run 这个方法,SpringBoot 即可将命令行的参数配置覆盖配置文件指定的配置。 接下来看看这个类做了什么事情:

1
2
3
4
5
6
7
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
return run(new Class<?>[] { primarySource }, args);
}

public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
return new SpringApplication(primarySources).run(args);
}

通过静态方法,传递一个 Class 类作为主资源,然后再传递给主资源数组的 run 方法。那么这个数组其实我们是可以传递多个主资源的,比如我们做项目的时候,想要每个模块包彼此分离,即可传递多个包的主资源路径。 new SpringApplication(primarySources).run(args); 才是真正的进入容器的准备阶段: 首先看看构造器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
// 初始化主资源链表,用于下面需要读取的时候可以遍历.
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
// 判断Web环境,有webFlux/web/普通三个环境,主要通过类路径是否带有相对应需要的类来判断,如果都没有则初始化为普通Java项目.
this.webApplicationType = WebApplicationType.deduceFromClasspath();
// 初始化环境,这个接口多用于web环境,因为需要从web上下文加载一些信息.
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
// 初始化监听器,监听容器生命周期中需要回调的函数
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
// 从运行堆栈中寻找运行这个类的main方法所在的类
this.mainApplicationClass = deduceMainApplicationClass();
}

加载不同模块的元信息

在上面的构造器中看到 setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));Spring 还是跟以前获取 handlers 一样。通过在 META-INF 类加载路径定义不同的 spring.factories,使用类加载器读取这些配置文件资源,解析,加载配置文件中的类,初始化对象,来共同完成业务。

1
2
3
4
5
6
7
8
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
ClassLoader classLoader = getClassLoader();
// Use names and ensure unique to protect against duplicates
Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
AnnotationAwareOrderComparator.sort(instances);
return instances;
}

通过 SpringFactoriesLoader.loadFactoryNames(type, classLoader),加载当前 ClassLoader 中的指定类,源码显示如何读取,后面的 getOrDefault(factoryTypeName, Collections.emptyList()) 还是比较好理解的,如果加载到工厂类的名字就返回不然返回个空集合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
String factoryTypeName = factoryType.getName();
return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
}

private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
// 相当于Map<String, List<String>>结构,先从缓存命中.
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null) {
return result;
}

/*
属性定义了元信息的路径以及名字
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
*/
try {
// 开始加载类路径下特定名字的元信息文件
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
result = new LinkedMultiValueMap<>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
// 通过UrlResource解析成Properties对象
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
// 遍历所有模块的指定文件,然后加入缓存
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryTypeName = ((String) entry.getKey()).trim();
for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
result.add(factoryTypeName, factoryImplementationName.trim());
}
}
}
cache.put(classLoader, result);
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}

启动项目SpringApplication.run

可以看到 run 的源码跟之前的 AbstractApplicationContext#refresh 的味道还是一样的,先加载一系列容器运行时需要的生命周期类(Spring 模块间的也有用户自定义的),然后 refreshContext(context) 刷新容器上下文。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public ConfigurableApplicationContext run(String... args) {
// 这是用来打印加载时间的工具类,暂时可以略过.
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
// 主要设置JVM虚拟机支持无设备情况下让awt可运行的属性
configureHeadlessProperty();
// 一. 加载所有SpringApplicationRunListeners并开始遍历所有Lintener启动监听回调函数
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// 二. 开始准备ConfigurableEnvironment环境
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
// 三. 创建应用上下文
context = createApplicationContext();
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
// 四. 做一些准备工作
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
// 五. 刷新上下文
refreshContext(context);
// 六. 刷新完成后做的一些清理、回调工作
afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
// 七. 启动完成后,调用所有SpringApplicationRunListener的完成启动的回调函数
listeners.started(context);
// 八. 主要处理ApplicationRunner和CommandLineRunner的回调
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}

try {
// 九. 运行时的SpringApplicationRunListener回调函数
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
return context;
}

装载SpringApplicationRunListener

1
2
3
4
5
private SpringApplicationRunListeners getRunListeners(String[] args) {
Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
return new SpringApplicationRunListeners(logger,
getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args));
}

首先我们看第一步,加载所有的 SpringApplicationRunListener 子类,这个加载方式跟上面所说的加载元信息是一样的,只不过指定加载 SpringApplicationRunListener.class 类。 我们知道,Spring 经常会定义很多生命周期回调,供用户根据需求切入框架。这次 SpringBoot 的生命周期是 SpringApplicationRunListener。 首先看看 SpringApplicationRunListener 定义哪些生命周期函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public interface SpringApplicationRunListener {

// 项目开始时调用
default void starting() {
}

// 环境准备好时
default void environmentPrepared(ConfigurableEnvironment environment) {
}

// 上下文准备好时
default void contextPrepared(ConfigurableApplicationContext context) {
}

// 上下文读取完成
default void contextLoaded(ConfigurableApplicationContext context) {
}

// 启动完成
default void started(ConfigurableApplicationContext context) {
}

// 项目运行时调用
default void running(ConfigurableApplicationContext context) {
}

// 项目失败时
default void failed(ConfigurableApplicationContext context, Throwable exception) {
}

}

准备ConfigurableEnvironment环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments) {
// 首先根据环境类型获取对象的配置实现对象
ConfigurableEnvironment environment = getOrCreateEnvironment();
// 配置ConversionService单例对象,以及判断有没有设置Profiles环境
configureEnvironment(environment, applicationArguments.getSourceArgs());
// 读取其他环境中(比如web.xml)读取的配置属性
ConfigurationPropertySources.attach(environment);
// 继续上面的SpringApplicationRunListener生命周期调用
listeners.environmentPrepared(environment);
// 把环境绑定到Binder中,Binder是Spring提供的一个记录对象的容器
bindToSpringApplication(environment);
if (!this.isCustomEnvironment) {
// 如果当前的配置环境和重新判断的环境不同,则转换成当前的环境(有可能在生命周期修改了加载的环境?)
environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
deduceEnvironmentClass());
}
// 重新读取特定环境的配置
ConfigurationPropertySources.attach(environment);
return environment;
}

private ConfigurableEnvironment getOrCreateEnvironment() {
if (this.environment != null) {
return this.environment;
}
switch (this.webApplicationType) {
case SERVLET:
return new StandardServletEnvironment();
case REACTIVE:
return new StandardReactiveWebEnvironment();
default:
return new StandardEnvironment();
}
}

然后在主线程中配置忽略 BooleanBean

1
2
3
4
5
6
7
8
9
10
11
12
13
public ConfigurableApplicationContext run(String... args) {
//...
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);
//...
}

private void configureIgnoreBeanInfo(ConfigurableEnvironment environment) {
if (System.getProperty(CachedIntrospectionResults.IGNORE_BEANINFO_PROPERTY_NAME) == null) {
Boolean ignore = environment.getProperty("spring.beaninfo.ignore", Boolean.class, Boolean.TRUE);
System.setProperty(CachedIntrospectionResults.IGNORE_BEANINFO_PROPERTY_NAME, ignore.toString());
}
}

创建上下文

1
2
3
4
context = createApplicationContext();
// 获取模块的错误解释器,用于项目启动的时候解析是什么错误
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);

根据不同环境创建不同的上下文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
protected ConfigurableApplicationContext createApplicationContext() {
Class<?> contextClass = this.applicationContextClass;
if (contextClass == null) {
try {
switch (this.webApplicationType) {
case SERVLET:
contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);
break;
case REACTIVE:
contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
break;
default:
contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
}
}
catch (ClassNotFoundException ex) {
throw new IllegalStateException(
"Unable create a default ApplicationContext, please specify an ApplicationContextClass", ex);
}
}
return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
}

Context准备工作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment,
SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
// 设置上面准备好的环境信息
context.setEnvironment(environment);
// 处理之前做一些事情,但是当前环境下,只是注册了一个 ConversionService 到 BeanFactory 中
postProcessApplicationContext(context);
// 调用之前注册的所有Initializers
applyInitializers(context);
// 再调用所有的listeners
listeners.contextPrepared(context);
if (this.logStartupInfo) {
logStartupInfo(context.getParent() == null);
logStartupProfileInfo(context);
}
// 注册SpringBoot独有的一些Bean
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
if (printedBanner != null) {
beanFactory.registerSingleton("springBootBanner", printedBanner);
}
// 接下来跟之前一样
if (beanFactory instanceof DefaultListableBeanFactory) {
((DefaultListableBeanFactory) beanFactory)
.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
}
if (this.lazyInitialization) {
context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
}
// Load the sources
Set<Object> sources = getAllSources();
Assert.notEmpty(sources, "Sources must not be empty");
// 根据我们传递的主类进行读取,放入BeanFactory中
load(context, sources.toArray(new Object[0]));
listeners.contextLoaded(context);
}

其实当前 postProcessApplicationContext 除了注册 ConversionService 以外其他事情都没做。

refreshContext刷新上下文

因为我要看看自动装配的问题,所以这个时候为了故事比较好说,我加了 mybatis-spring-boot-starter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.BUILD-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.liweidan.web</groupId>
<artifactId>web-test</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>web-test</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.197</version>
</dependency>

</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

刷新上下文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void refreshContext(ConfigurableApplicationContext context) {
refresh(context);
if (this.registerShutdownHook) {
try {
context.registerShutdownHook();
}
catch (AccessControlException ex) {
// Not allowed in some environments.
}
}
}
protected void refresh(ApplicationContext applicationContext) {
Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext);
((AbstractApplicationContext) applicationContext).refresh();
}

然后接下来就去到我们熟悉的 AbstractApplicationContext 中,重温一下这个 refresh() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
prepareRefresh();

// Tell the subclass to refresh the internal bean factory.
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);

try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);

// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);

// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);

// Initialize message source for this context.
initMessageSource();

// Initialize event multicaster for this context.
initApplicationEventMulticaster();

// Initialize other special beans in specific context subclasses.
onRefresh();

// Check for listener beans and register them.
registerListeners();

// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);

// Last step: publish corresponding event.
finishRefresh();
}

catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}

// Destroy already created singletons to avoid dangling resources.
destroyBeans();

// Reset 'active' flag.
cancelRefresh(ex);

// Propagate exception to caller.
throw ex;
}

finally {
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
resetCommonCaches();
}
}
}

同之前一样,在 invokeBeanFactoryPostProcessors(beanFactory) 这句话出现了 BeanDefinition 数量的剧增,那么我们可以推断出,这句话还是关键,就是在这句话开始解析我们的项目依赖。

其实按照我的猜测,自动配置应该是 starter 提供了一些配置类交给 SpringBoot 注入 Spring 的 BeanFactory 中,但是现在看来,好像 SpringBoot 就不需要做什么东西,直接委托给容器扫描解析执行了。

invokeBeanFactoryPostProcessors

1
2
3
4
5
6
7
8
9
10
11
protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
// 利用委派模式委托给 PostProcessorRegistrationDelegate 进行执行
PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());

// Detect a LoadTimeWeaver and prepare for weaving, if found in the meantime
// (e.g. through an @Bean method registered by ConfigurationClassPostProcessor)
if (beanFactory.getTempClassLoader() == null && beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) {
beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));
beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
}
}

PostProcessorRegistrationDelegate委派对象

这是一个在 Context 模块下对象,主要处理 PostProcessor 相关的事情,看到签名只提供两个方法:

  • invokeBeanFactoryPostProcessors 执行 BF 的 PostProcessors
  • registerBeanPostProcessors 注册 PostProcessors 到 BF 中

关于普通的配置类解析,之前在 chapters3_高级的Beafctory—Spring上下文 已经有提到,解析项目中配置类不明白的话可以先看看那篇文章。 不过我们现在重点是怎么读取到 starter 框架里边的自动配置信息:

1
2
3
4
5
6
7
private static void invokeBeanDefinitionRegistryPostProcessors(
Collection<? extends BeanDefinitionRegistryPostProcessor> postProcessors, BeanDefinitionRegistry registry) {

for (BeanDefinitionRegistryPostProcessor postProcessor : postProcessors) {
postProcessor.postProcessBeanDefinitionRegistry(registry);
}
}

当前 postProcessors 里边只有一个 ConfigurationClassPostProcessor 那我们进入对应的 postProcessBeanDefinitionRegistry 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
int registryId = System.identityHashCode(registry);
if (this.registriesPostProcessed.contains(registryId)) {
throw new IllegalStateException(
"postProcessBeanDefinitionRegistry already called on this post-processor against " + registry);
}
if (this.factoriesPostProcessed.contains(registryId)) {
throw new IllegalStateException(
"postProcessBeanFactory already called on this post-processor against " + registry);
}
this.registriesPostProcessed.add(registryId);

processConfigBeanDefinitions(registry);
}

processConfigBeanDefinitions 解析配置

由于我们现在需要跟自动导入的配置,所以我们应该需要跟的类配置是我们的主类,也就是 WebTestApplication 这个类,这个类上修饰的注解 @SpringBootApplication 是自动注入的关键

这个才是执行自动配置的重点之重:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
/**
* Build and validate a configuration model based on the registry of
* {@link Configuration} classes.
*/
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
List<BeanDefinitionHolder> configCandidates = new ArrayList<>();
String[] candidateNames = registry.getBeanDefinitionNames();

for (String beanName : candidateNames) {
BeanDefinition beanDef = registry.getBeanDefinition(beanName);
if (beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE) != null) {
if (logger.isDebugEnabled()) {
logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);
}
}
else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
}
}

// Return immediately if no @Configuration classes were found
if (configCandidates.isEmpty()) {
return;
}

// Sort by previously determined @Order value, if applicable
configCandidates.sort((bd1, bd2) -> {
int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition());
int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition());
return Integer.compare(i1, i2);
});

// Detect any custom bean name generation strategy supplied through the enclosing application context
SingletonBeanRegistry sbr = null;
if (registry instanceof SingletonBeanRegistry) {
sbr = (SingletonBeanRegistry) registry;
if (!this.localBeanNameGeneratorSet) {
BeanNameGenerator generator = (BeanNameGenerator) sbr.getSingleton(
AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR);
if (generator != null) {
this.componentScanBeanNameGenerator = generator;
this.importBeanNameGenerator = generator;
}
}
}

if (this.environment == null) {
this.environment = new StandardEnvironment();
}

// Parse each @Configuration class
ConfigurationClassParser parser = new ConfigurationClassParser(
this.metadataReaderFactory, this.problemReporter, this.environment,
this.resourceLoader, this.componentScanBeanNameGenerator, registry);

Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());
// ↑ 这上面属于解析开始的准备工作 ↑
do {
parser.parse(candidates);
parser.validate();

Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
configClasses.removeAll(alreadyParsed);

// Read the model and create bean definitions based on its content
if (this.reader == null) {
this.reader = new ConfigurationClassBeanDefinitionReader(
registry, this.sourceExtractor, this.resourceLoader, this.environment,
this.importBeanNameGenerator, parser.getImportRegistry());
}
this.reader.loadBeanDefinitions(configClasses);
alreadyParsed.addAll(configClasses);

candidates.clear();
if (registry.getBeanDefinitionCount() > candidateNames.length) {
String[] newCandidateNames = registry.getBeanDefinitionNames();
Set<String> oldCandidateNames = new HashSet<>(Arrays.asList(candidateNames));
Set<String> alreadyParsedClasses = new HashSet<>();
for (ConfigurationClass configurationClass : alreadyParsed) {
alreadyParsedClasses.add(configurationClass.getMetadata().getClassName());
}
for (String candidateName : newCandidateNames) {
if (!oldCandidateNames.contains(candidateName)) {
BeanDefinition bd = registry.getBeanDefinition(candidateName);
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, this.metadataReaderFactory) &&
!alreadyParsedClasses.contains(bd.getBeanClassName())) {
candidates.add(new BeanDefinitionHolder(bd, candidateName));
}
}
}
candidateNames = newCandidateNames;
}
}
while (!candidates.isEmpty());

// Register the ImportRegistry as a bean in order to support ImportAware @Configuration classes
if (sbr != null && !sbr.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) {
sbr.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry());
}

if (this.metadataReaderFactory instanceof CachingMetadataReaderFactory) {
// Clear cache in externally provided MetadataReaderFactory; this is a no-op
// for a shared cache since it'll be cleared by the ApplicationContext.
((CachingMetadataReaderFactory) this.metadataReaderFactory).clearCache();
}
}

重点看看 parser.parse(candidates); 这句话是怎么解析的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public void parse(Set<BeanDefinitionHolder> configCandidates) {
for (BeanDefinitionHolder holder : configCandidates) {
BeanDefinition bd = holder.getBeanDefinition();
try {
if (bd instanceof AnnotatedBeanDefinition) {
// 因为这是一个使用注解的Bean,所以应该关注这里
parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
}
else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) {
parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName());
}
else {
parse(bd.getBeanClassName(), holder.getBeanName());
}
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex);
}
}

this.deferredImportSelectorHandler.process();
}

protected final void parse(AnnotationMetadata metadata, String beanName) throws IOException {
processConfigurationClass(new ConfigurationClass(metadata, beanName));
}

protected void processConfigurationClass(ConfigurationClass configClass) throws IOException {
// 如果当前的类是一个解析类,则跳过解析
if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {
return;
}

// 判断是否已经解析过了
ConfigurationClass existingClass = this.configurationClasses.get(configClass);
if (existingClass != null) {
if (configClass.isImported()) {
if (existingClass.isImported()) {
existingClass.mergeImportedBy(configClass);
}
// Otherwise ignore new imported config class; existing non-imported class overrides it.
return;
}
else {
// Explicit bean definition found, probably replacing an import.
// Let's remove the old one and go with the new one.
this.configurationClasses.remove(configClass);
this.knownSuperclasses.values().removeIf(configClass::equals);
}
}

// 递归解析配置类以及他的父类、接口等等
SourceClass sourceClass = asSourceClass(configClass);
do {
sourceClass = doProcessConfigurationClass(configClass, sourceClass);
}
while (sourceClass != null);

this.configurationClasses.put(configClass, configClass);
}

然后我们要进入 doProcessConfigurationClass 观察解析的过程。 这串代码其实有点长……:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
@Nullable
protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)
throws IOException {

if (configClass.getMetadata().isAnnotated(Component.class.getName())) {
// Recursively process any member (nested) classes first
processMemberClasses(configClass, sourceClass);
}

// 解析需要导入Property配置文件的类
for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), PropertySources.class,
org.springframework.context.annotation.PropertySource.class)) {
if (this.environment instanceof ConfigurableEnvironment) {
processPropertySource(propertySource);
}
else {
logger.info("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +
"]. Reason: Environment must implement ConfigurableEnvironment");
}
}

// 解析 @ComponentScan
Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
if (!componentScans.isEmpty() &&
!this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
for (AnnotationAttributes componentScan : componentScans) {
// The config class is annotated with @ComponentScan -> perform the scan immediately
Set<BeanDefinitionHolder> scannedBeanDefinitions =
this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
// Check the set of scanned definitions for any further config classes and parse recursively if needed
for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
if (bdCand == null) {
bdCand = holder.getBeanDefinition();
}
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
parse(bdCand.getBeanClassName(), holder.getBeanName());
}
}
}
}

// 解析 @Import 的类,这句话是重点
processImports(configClass, sourceClass, getImports(sourceClass), true);

// Process any @ImportResource annotations
AnnotationAttributes importResource =
AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class);
if (importResource != null) {
String[] resources = importResource.getStringArray("locations");
Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader");
for (String resource : resources) {
String resolvedResource = this.environment.resolveRequiredPlaceholders(resource);
configClass.addImportedResource(resolvedResource, readerClass);
}
}

// 解析 @Bean 方法
Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);
for (MethodMetadata methodMetadata : beanMethods) {
configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
}

// 解析接口默认方法
processInterfaces(configClass, sourceClass);

// 如果有父类,返回父类,由上一层继续调用这个方法继续解析
if (sourceClass.getMetadata().hasSuperClass()) {
String superclass = sourceClass.getMetadata().getSuperClassName();
if (superclass != null && !superclass.startsWith("java") &&
!this.knownSuperclasses.containsKey(superclass)) {
this.knownSuperclasses.put(superclass, configClass);
// Superclass found, return its annotation metadata and recurse
return sourceClass.getSuperClass();
}
}

// 返回Null表示解析结束
return null;
}

好了,可以看到通过 Java 类的解析可谓覆盖到尼玛我想象不到的地方….但他们就是应该要有 挑重点来看吧,我的主类没有父级也没有接口,直接重点看 processImports(configClass, sourceClass, getImports(sourceClass), true) 这句话。 这句话首先需要获取所有的导入配置 getImports(sourceClass)

1
2
3
4
5
6
private Set<SourceClass> getImports(SourceClass sourceClass) throws IOException {
Set<SourceClass> imports = new LinkedHashSet<>();
Set<SourceClass> visited = new LinkedHashSet<>();
collectImports(sourceClass, imports, visited);
return imports;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
private void collectImports(SourceClass sourceClass, Set<SourceClass> imports, Set<SourceClass> visited)
throws IOException {

if (visited.add(sourceClass)) {
for (SourceClass annotation : sourceClass.getAnnotations()) {
String annName = annotation.getMetadata().getClassName();
if (!annName.equals(Import.class.getName())) {
collectImports(annotation, imports, visited);
}
}
imports.addAll(sourceClass.getAnnotationAttributes(Import.class.getName(), "value"));
}
}

这两个东西,切忌不要去 debug 一个一个看….我在这里看晕了好几个小时,应该是条件断点判断 sourceClass.getAnnotationAttributes(Import.class.getName(), "value").size() > 0 再停住

好了等到断点停住的时候,发现是 @interface SpringBootApplication > @interface EnableAutoConfiguration 上面的 @Import(AutoConfigurationImportSelector.class) 在这里导入了。以及这个类上边的 @AutoConfigurationPackage 导入了 @Import(AutoConfigurationPackages.Registrar.class) 所以当前需要解析的 importCandidatesAutoConfigurationPackages.Registrar.class 以及 AutoConfigurationImportSelector.class 两个类。 那么这两个类是什么用的,我现在也还不知道…先知道他被导入就好了,接下去看 processImports 方法了,这个方法应该有答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
Collection<SourceClass> importCandidates, boolean checkForCircularImports) {

if (importCandidates.isEmpty()) {
return;
}

// 由上面传递的 checkForCircularImports 参数决定是否判断有没有循环引入
if (checkForCircularImports && isChainedImportOnStack(configClass)) {
this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
}
else {
this.importStack.push(configClass);
try {
// 开始解析导入类
for (SourceClass candidate : importCandidates) {
// 第一层,根据不同的导入类型进行解析
// AutoConfigurationImportSelector 来到这个分支
if (candidate.isAssignable(ImportSelector.class)) {
// Candidate class is an ImportSelector -> delegate to it to determine imports
Class<?> candidateClass = candidate.loadClass();
ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class,
this.environment, this.resourceLoader, this.registry);
if (selector instanceof DeferredImportSelector) {
this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
}
else {
String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames);
// 递归解析
processImports(configClass, currentSourceClass, importSourceClasses, false);
}
}
// AutoConfigurationPackages.Registrar进入这个分支
else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
// Candidate class is an ImportBeanDefinitionRegistrar ->
// delegate to it to register additional bean definitions
Class<?> candidateClass = candidate.loadClass();
ImportBeanDefinitionRegistrar registrar =
ParserStrategyUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class,
this.environment, this.resourceLoader, this.registry);
configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
}
else {
// 直接以@Configuration的方式进行处理
this.importStack.registerImport(
currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
processConfigurationClass(candidate.asConfigClass(configClass));
}
}
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to process import candidates for configuration class [" +
configClass.getMetadata().getClassName() + "]", ex);
}
finally {
this.importStack.pop();
}
}
}

不行呀,因为需要根据类型来做,先给两个类的类型吧:

SpringBoot

OK,现代 Java 开发者应该都对 SpringBoot 很熟悉了吧,一个很 "轻量级"IOC 容器。 我记得 SpringBoot 刚出来的时候,很多博客文章都会说 SpringBoot 减轻了 Java 开发工作者的负担,是个轻量级的框架。然而后面我才发现,并不轻量级。因为,SpringBoot 把需要依赖的东西给封装了起来,但其实比起之前自己控制依赖项目来说,反而会更重了一些,毕竟以前还可以自由组合。现在,SpringBoot 以及框架作者都提供了默认的依赖以及默认的配置,所以说这个框架轻量级其实并不是,要说轻量级应该只是说开发轻量级而已,菜鸟也可以快速上手建立一个后台项目而不必去关心太多项目配置的东西。 框架肯定是一个优秀的框架,我们项目全体也都是 SpringBoot 架构起来的,所以还是需要看看,SpringBoot 偷偷帮我们做了什么事情。

依赖简化

日常使用中,我们只需要引入一个 starter,就可以神奇把我们整合的框架整合起来。但是在以前,我们要使用 MyBatis 的时候,却需要引入 org.mybatis:mybatis 主框架,然后还因为需要整合 Spring 框架,所以我们还需要引入一个 org.mybatis:mybatis-spring。这就算了,这两个包如果版本号对不上,还要出现兼容性的问题。 然而现在我们只需要一个 org.mybatis.spring.boot:mybatis-spring-boot-starter,就可以同时引入这两个包,而且版本号还解决的很OK。所以现在我们可以看看他的 pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot</artifactId>
<version>1.3.1</version>
</parent>
<artifactId>mybatis-spring-boot-starter</artifactId>
<name>mybatis-spring-boot-starter</name>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
</dependency>
</dependencies>
</project>

所以结论是,一个 starter,定义了需要依赖的包的版本,然后通过依赖传递将这些包传递到我们 starter 所在的项目上来。 目前我建立了一个项目,这个项目很简单,只是依赖了一个 spring-boot-starter-web, 我们可以在 idea 上很方便的查看依赖的所有东西:

但其实这个做法在以前 Spring 的时候是已经存在了,这个项目就是 Spring-IO,但我也不知道为什么,可能因为名字取得不够好吧?然后这个项目好像很少人用。或者以前的人觉得会被依赖很多东西进来所以不用这个项目了?

自动装配

我们项目所需要的依赖都弄进来了,下一步就是配置让这些包可以相互配合,共同提供服务呀。 所以,SpringBoot 所提供的第二个功能就是,根据默认的配置,装配依赖进来包里面的 Bean 实例。依赖+装配,就是我们以前一直所需要的操作了,可以说这个框架减少了全世界程序员按 CTRL+C/V 的次数hhhhh。 OK,简单了解的话,我需要一个额外的 starter,这里就使用 org.mybatis.spring.boot:mybatis-spring-boot-starter 来看,仓库地址 项目的目录结构为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
├── LICENSE
├── README.md
├── license.txt
├── mvnw
├── mvnw.cmd
├── mybatis-spring-boot-autoconfigure
│   ├── format.xml
│   ├── license.txt
│   ├── pom.xml
│   └── src
├── mybatis-spring-boot-samples
│   ├── mybatis-spring-boot-sample-annotation
│   ├── mybatis-spring-boot-sample-freemarker
│   ├── mybatis-spring-boot-sample-freemarker-legacy
│   ├── mybatis-spring-boot-sample-groovy
│   ├── mybatis-spring-boot-sample-kotlin
│   ├── mybatis-spring-boot-sample-thymeleaf
│   ├── mybatis-spring-boot-sample-velocity
│   ├── mybatis-spring-boot-sample-velocity-legacy
│   ├── mybatis-spring-boot-sample-war
│   ├── mybatis-spring-boot-sample-web
│   ├── mybatis-spring-boot-sample-xml
│   └── pom.xml
├── mybatis-spring-boot-starter
│   ├── license.txt
│   └── pom.xml
├── mybatis-spring-boot-starter-test
│   └── pom.xml
├── mybatis-spring-boot-test-autoconfigure
│   ├── format.xml
│   ├── pom.xml
│   └── src
├── pom.xml
└── travis
├── after_success.sh
└── settings.xml

其中,mybatis-spring-boot-autoconfigure 比较惹人注目,所以我们现在就看看这个项目。 首先我们看看 main/resources/META-INF/spring.factories,因为 Spring 很喜欢通过这些 meta 文件来促使各个模块很好的解耦但又能彼此配合工作,所以这个文件是定义 自动装配 开始的工厂类:

1
2
3
4
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration,\
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration

那么通过看初步的源码,这两个类都是熟悉的 Java 装配类,那么将会被加入到前面提到的 BeanFactory 容器中,后面解析将会调用里面的配置方法。 第二个优秀的地方就是,定义配置类,就可以在 yaml 文件中提示的出现,可以利用 IDE 工具很好的防止配置名写错。这个类就是 MybatisProperties.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@ConfigurationProperties(prefix = MybatisProperties.MYBATIS_PREFIX)
public class MybatisProperties {

public static final String MYBATIS_PREFIX = "mybatis";

private static final ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();

/**
* Location of MyBatis xml config file.
*/
private String configLocation;

/**
* Locations of MyBatis mapper files.
*/
private String[] mapperLocations;

...
}

OK,我们可以看到,定义了前缀就是 mybatis,而这个类是个贫血型的 Bean,只有属性。这些属性将会贯穿 MyBatis 在项目中整个生命周期。 接下来一个问题,我并不需要每个属性都在 yaml 文件中去定义啊,有些直接使用官方提供的默认值就可以了。 所以官方又贴心的提供了 additional-spring-configuration-metadata.json 这个 JSON 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
{
"properties": [
{
"sourceType": "org.apache.ibatis.session.Configuration",
"defaultValue": "org.apache.ibatis.scripting.xmltags.XMLLanguageDriver",
"name": "mybatis.configuration.default-scripting-language",
"description": "A default LanguageDriver class.",
"type": "java.lang.Class<? extends org.apache.ibatis.scripting.LanguageDriver>",
"deprecation": {
"reason": "Because when this configuration property is used, there is case that custom language driver cannot be registered correctly.",
"replacement": "mybatis.default-scripting-language-driver"
}
},
{
"sourceType": "org.apache.ibatis.session.Configuration",
"defaultValue": "org.apache.ibatis.type.EnumTypeHandler",
"name": "mybatis.configuration.default-enum-type-handler",
"description": "A default TypeHandler class for Enum.",
"type": "java.lang.Class<? extends org.apache.ibatis.type.TypeHandler>"
},
{
"defaultValue": false,
"name": "mybatis.lazy-initialization",
"description": "Set whether enable lazy initialization for mapper bean.",
"type": "java.lang.Boolean"
},
{
"name": "mybatis.scripting-language-driver.velocity.userdirective",
"deprecation": {
"level": "error",
"reason": "The 'userdirective' is deprecated since Velocity 2.x. This property defined for keeping backward compatibility with older velocity version.",
"replacement": "mybatis.scripting-language-driver.velocity.velocity-settings.runtime.custom_directives"
}
}

]
}

不好理解的话,我们直接看第三个默认配置就好了:

1
2
3
4
5
6
{
"defaultValue": false,
"name": "mybatis.lazy-initialization",
"description": "Set whether enable lazy initialization for mapper bean.",
"type": "java.lang.Boolean"
}

看了下图应该明白了吧,上面那个文件就是定义配置的一些说明、默认值的。

所以当我们要造一个框架,又因为很多约定的东西,靠人脑来记已经靠不住的情况下,就可以编写类似于 additional-spring-configuration-metadata.json 这种文件来做约定以及说明了。


OK,上面说完了 SpringBoot 的两个最主要的优点以后,现在就来看看源码。但是源码这块,因为 传递依赖 利用的是 mvn/gradle 的特性,所以 依赖传递 并不需要再说。 那么最主要的就是来解析,怎么自动装配的问题。

简单的SpringBoot项目

pom.xml 依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.BUILD-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.liweidan.web</groupId>
<artifactId>web-test</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>web-test</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

一个主启动器:

1
2
3
4
5
6
7
8
@SpringBootApplication
public class WebTestApplication {

public static void main(String[] args) {
SpringApplication.run(WebTestApplication.class, args);
}

}

一个控制器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
public class UserController {

@GetMapping("users")
public List<Map<String, String>> users() {
List<Map<String, String>> users = new ArrayList<>();
for (int i = 0; i < 3; i++) {
Map<String, String> u = new HashMap<>();
u.put("id", String.valueOf(i));
u.put("name", "name" + i);
users.add(u);
}
return users;
}

}

SpringApplication元信息准备

写过 SpringBoot 项目的同学应该对这个很熟悉了,通过传递一个上下文的根类,SpringBoot 将会自动装载在此类所在的包下面的所有类,并且 args 很明显就是我们在控制台传递的参数,也一并传递给 SpringApplication.run 这个方法,SpringBoot 即可将命令行的参数配置覆盖配置文件指定的配置。 接下来看看这个类做了什么事情:

1
2
3
4
5
6
7
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
return run(new Class<?>[] { primarySource }, args);
}

public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
return new SpringApplication(primarySources).run(args);
}

通过静态方法,传递一个 Class 类作为主资源,然后再传递给主资源数组的 run 方法。那么这个数组其实我们是可以传递多个主资源的,比如我们做项目的时候,想要每个模块包彼此分离,即可传递多个包的主资源路径。 new SpringApplication(primarySources).run(args); 才是真正的进入容器的准备阶段: 首先看看构造器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
// 初始化主资源链表,用于下面需要读取的时候可以遍历.
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
// 判断Web环境,有webFlux/web/普通三个环境,主要通过类路径是否带有相对应需要的类来判断,如果都没有则初始化为普通Java项目.
this.webApplicationType = WebApplicationType.deduceFromClasspath();
// 初始化环境,这个接口多用于web环境,因为需要从web上下文加载一些信息.
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
// 初始化监听器,监听容器生命周期中需要回调的函数
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
// 从运行堆栈中寻找运行这个类的main方法所在的类
this.mainApplicationClass = deduceMainApplicationClass();
}

加载不同模块的元信息

在上面的构造器中看到 setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));Spring 还是跟以前获取 handlers 一样。通过在 META-INF 类加载路径定义不同的 spring.factories,使用类加载器读取这些配置文件资源,解析,加载配置文件中的类,初始化对象,来共同完成业务。

1
2
3
4
5
6
7
8
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
ClassLoader classLoader = getClassLoader();
// Use names and ensure unique to protect against duplicates
Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
AnnotationAwareOrderComparator.sort(instances);
return instances;
}

通过 SpringFactoriesLoader.loadFactoryNames(type, classLoader),加载当前 ClassLoader 中的指定类,源码显示如何读取,后面的 getOrDefault(factoryTypeName, Collections.emptyList()) 还是比较好理解的,如果加载到工厂类的名字就返回不然返回个空集合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
String factoryTypeName = factoryType.getName();
return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
}

private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
// 相当于Map<String, List<String>>结构,先从缓存命中.
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null) {
return result;
}

/*
属性定义了元信息的路径以及名字
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
*/
try {
// 开始加载类路径下特定名字的元信息文件
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
result = new LinkedMultiValueMap<>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
// 通过UrlResource解析成Properties对象
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
// 遍历所有模块的指定文件,然后加入缓存
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryTypeName = ((String) entry.getKey()).trim();
for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
result.add(factoryTypeName, factoryImplementationName.trim());
}
}
}
cache.put(classLoader, result);
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}

启动项目SpringApplication.run

可以看到 run 的源码跟之前的 AbstractApplicationContext#refresh 的味道还是一样的,先加载一系列容器运行时需要的生命周期类(Spring 模块间的也有用户自定义的),然后 refreshContext(context) 刷新容器上下文。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public ConfigurableApplicationContext run(String... args) {
// 这是用来打印加载时间的工具类,暂时可以略过.
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
// 主要设置JVM虚拟机支持无设备情况下让awt可运行的属性
configureHeadlessProperty();
// 一. 加载所有SpringApplicationRunListeners并开始遍历所有Lintener启动监听回调函数
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// 二. 开始准备ConfigurableEnvironment环境
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
// 三. 创建应用上下文
context = createApplicationContext();
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
// 四. 做一些准备工作
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
// 五. 刷新上下文
refreshContext(context);
// 六. 刷新完成后做的一些清理、回调工作
afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
// 七. 启动完成后,调用所有SpringApplicationRunListener的完成启动的回调函数
listeners.started(context);
// 八. 主要处理ApplicationRunner和CommandLineRunner的回调
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}

try {
// 九. 运行时的SpringApplicationRunListener回调函数
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
return context;
}

装载SpringApplicationRunListener

1
2
3
4
5
private SpringApplicationRunListeners getRunListeners(String[] args) {
Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
return new SpringApplicationRunListeners(logger,
getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args));
}

首先我们看第一步,加载所有的 SpringApplicationRunListener 子类,这个加载方式跟上面所说的加载元信息是一样的,只不过指定加载 SpringApplicationRunListener.class 类。 我们知道,Spring 经常会定义很多生命周期回调,供用户根据需求切入框架。这次 SpringBoot 的生命周期是 SpringApplicationRunListener。 首先看看 SpringApplicationRunListener 定义哪些生命周期函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public interface SpringApplicationRunListener {

// 项目开始时调用
default void starting() {
}

// 环境准备好时
default void environmentPrepared(ConfigurableEnvironment environment) {
}

// 上下文准备好时
default void contextPrepared(ConfigurableApplicationContext context) {
}

// 上下文读取完成
default void contextLoaded(ConfigurableApplicationContext context) {
}

// 启动完成
default void started(ConfigurableApplicationContext context) {
}

// 项目运行时调用
default void running(ConfigurableApplicationContext context) {
}

// 项目失败时
default void failed(ConfigurableApplicationContext context, Throwable exception) {
}

}

准备ConfigurableEnvironment环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments) {
// 首先根据环境类型获取对象的配置实现对象
ConfigurableEnvironment environment = getOrCreateEnvironment();
// 配置ConversionService单例对象,以及判断有没有设置Profiles环境
configureEnvironment(environment, applicationArguments.getSourceArgs());
// 读取其他环境中(比如web.xml)读取的配置属性
ConfigurationPropertySources.attach(environment);
// 继续上面的SpringApplicationRunListener生命周期调用
listeners.environmentPrepared(environment);
// 把环境绑定到Binder中,Binder是Spring提供的一个记录对象的容器
bindToSpringApplication(environment);
if (!this.isCustomEnvironment) {
// 如果当前的配置环境和重新判断的环境不同,则转换成当前的环境(有可能在生命周期修改了加载的环境?)
environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
deduceEnvironmentClass());
}
// 重新读取特定环境的配置
ConfigurationPropertySources.attach(environment);
return environment;
}

private ConfigurableEnvironment getOrCreateEnvironment() {
if (this.environment != null) {
return this.environment;
}
switch (this.webApplicationType) {
case SERVLET:
return new StandardServletEnvironment();
case REACTIVE:
return new StandardReactiveWebEnvironment();
default:
return new StandardEnvironment();
}
}

然后在主线程中配置忽略 BooleanBean

1
2
3
4
5
6
7
8
9
10
11
12
13
public ConfigurableApplicationContext run(String... args) {
//...
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);
//...
}

private void configureIgnoreBeanInfo(ConfigurableEnvironment environment) {
if (System.getProperty(CachedIntrospectionResults.IGNORE_BEANINFO_PROPERTY_NAME) == null) {
Boolean ignore = environment.getProperty("spring.beaninfo.ignore", Boolean.class, Boolean.TRUE);
System.setProperty(CachedIntrospectionResults.IGNORE_BEANINFO_PROPERTY_NAME, ignore.toString());
}
}

创建上下文

1
2
3
4
context = createApplicationContext();
// 获取模块的错误解释器,用于项目启动的时候解析是什么错误
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);

根据不同环境创建不同的上下文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
protected ConfigurableApplicationContext createApplicationContext() {
Class<?> contextClass = this.applicationContextClass;
if (contextClass == null) {
try {
switch (this.webApplicationType) {
case SERVLET:
contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);
break;
case REACTIVE:
contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
break;
default:
contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
}
}
catch (ClassNotFoundException ex) {
throw new IllegalStateException(
"Unable create a default ApplicationContext, please specify an ApplicationContextClass", ex);
}
}
return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
}

Context准备工作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment,
SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
// 设置上面准备好的环境信息
context.setEnvironment(environment);
// 处理之前做一些事情,但是当前环境下,只是注册了一个 ConversionService 到 BeanFactory 中
postProcessApplicationContext(context);
// 调用之前注册的所有Initializers
applyInitializers(context);
// 再调用所有的listeners
listeners.contextPrepared(context);
if (this.logStartupInfo) {
logStartupInfo(context.getParent() == null);
logStartupProfileInfo(context);
}
// 注册SpringBoot独有的一些Bean
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
if (printedBanner != null) {
beanFactory.registerSingleton("springBootBanner", printedBanner);
}
// 接下来跟之前一样
if (beanFactory instanceof DefaultListableBeanFactory) {
((DefaultListableBeanFactory) beanFactory)
.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
}
if (this.lazyInitialization) {
context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
}
// Load the sources
Set<Object> sources = getAllSources();
Assert.notEmpty(sources, "Sources must not be empty");
// 根据我们传递的主类进行读取,放入BeanFactory中
load(context, sources.toArray(new Object[0]));
listeners.contextLoaded(context);
}

其实当前 postProcessApplicationContext 除了注册 ConversionService 以外其他事情都没做。

refreshContext刷新上下文

因为我要看看自动装配的问题,所以这个时候为了故事比较好说,我加了 mybatis-spring-boot-starter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.BUILD-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.liweidan.web</groupId>
<artifactId>web-test</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>web-test</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.197</version>
</dependency>

</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

刷新上下文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void refreshContext(ConfigurableApplicationContext context) {
refresh(context);
if (this.registerShutdownHook) {
try {
context.registerShutdownHook();
}
catch (AccessControlException ex) {
// Not allowed in some environments.
}
}
}
protected void refresh(ApplicationContext applicationContext) {
Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext);
((AbstractApplicationContext) applicationContext).refresh();
}

然后接下来就去到我们熟悉的 AbstractApplicationContext 中,重温一下这个 refresh() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
prepareRefresh();

// Tell the subclass to refresh the internal bean factory.
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);

try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);

// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);

// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);

// Initialize message source for this context.
initMessageSource();

// Initialize event multicaster for this context.
initApplicationEventMulticaster();

// Initialize other special beans in specific context subclasses.
onRefresh();

// Check for listener beans and register them.
registerListeners();

// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);

// Last step: publish corresponding event.
finishRefresh();
}

catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}

// Destroy already created singletons to avoid dangling resources.
destroyBeans();

// Reset 'active' flag.
cancelRefresh(ex);

// Propagate exception to caller.
throw ex;
}

finally {
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
resetCommonCaches();
}
}
}

同之前一样,在 invokeBeanFactoryPostProcessors(beanFactory) 这句话出现了 BeanDefinition 数量的剧增,那么我们可以推断出,这句话还是关键,就是在这句话开始解析我们的项目依赖。

其实按照我的猜测,自动配置应该是 starter 提供了一些配置类交给 SpringBoot 注入 Spring 的 BeanFactory 中,但是现在看来,好像 SpringBoot 就不需要做什么东西,直接委托给容器扫描解析执行了。

invokeBeanFactoryPostProcessors

1
2
3
4
5
6
7
8
9
10
11
protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
// 利用委派模式委托给 PostProcessorRegistrationDelegate 进行执行
PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());

// Detect a LoadTimeWeaver and prepare for weaving, if found in the meantime
// (e.g. through an @Bean method registered by ConfigurationClassPostProcessor)
if (beanFactory.getTempClassLoader() == null && beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) {
beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));
beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
}
}

PostProcessorRegistrationDelegate委派对象

这是一个在 Context 模块下对象,主要处理 PostProcessor 相关的事情,看到签名只提供两个方法:

  • invokeBeanFactoryPostProcessors 执行 BF 的 PostProcessors
  • registerBeanPostProcessors 注册 PostProcessors 到 BF 中

关于普通的配置类解析,之前在 chapters3_高级的Beafctory—Spring上下文 已经有提到,解析项目中配置类不明白的话可以先看看那篇文章。 不过我们现在重点是怎么读取到 starter 框架里边的自动配置信息:

1
2
3
4
5
6
7
private static void invokeBeanDefinitionRegistryPostProcessors(
Collection<? extends BeanDefinitionRegistryPostProcessor> postProcessors, BeanDefinitionRegistry registry) {

for (BeanDefinitionRegistryPostProcessor postProcessor : postProcessors) {
postProcessor.postProcessBeanDefinitionRegistry(registry);
}
}

当前 postProcessors 里边只有一个 ConfigurationClassPostProcessor 那我们进入对应的 postProcessBeanDefinitionRegistry 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
int registryId = System.identityHashCode(registry);
if (this.registriesPostProcessed.contains(registryId)) {
throw new IllegalStateException(
"postProcessBeanDefinitionRegistry already called on this post-processor against " + registry);
}
if (this.factoriesPostProcessed.contains(registryId)) {
throw new IllegalStateException(
"postProcessBeanFactory already called on this post-processor against " + registry);
}
this.registriesPostProcessed.add(registryId);

processConfigBeanDefinitions(registry);
}

processConfigBeanDefinitions 解析配置

由于我们现在需要跟自动导入的配置,所以我们应该需要跟的类配置是我们的主类,也就是 WebTestApplication 这个类,这个类上修饰的注解 @SpringBootApplication 是自动注入的关键

这个才是执行自动配置的重点之重:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
/**
* Build and validate a configuration model based on the registry of
* {@link Configuration} classes.
*/
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
List<BeanDefinitionHolder> configCandidates = new ArrayList<>();
String[] candidateNames = registry.getBeanDefinitionNames();

for (String beanName : candidateNames) {
BeanDefinition beanDef = registry.getBeanDefinition(beanName);
if (beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE) != null) {
if (logger.isDebugEnabled()) {
logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);
}
}
else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
}
}

// Return immediately if no @Configuration classes were found
if (configCandidates.isEmpty()) {
return;
}

// Sort by previously determined @Order value, if applicable
configCandidates.sort((bd1, bd2) -> {
int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition());
int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition());
return Integer.compare(i1, i2);
});

// Detect any custom bean name generation strategy supplied through the enclosing application context
SingletonBeanRegistry sbr = null;
if (registry instanceof SingletonBeanRegistry) {
sbr = (SingletonBeanRegistry) registry;
if (!this.localBeanNameGeneratorSet) {
BeanNameGenerator generator = (BeanNameGenerator) sbr.getSingleton(
AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR);
if (generator != null) {
this.componentScanBeanNameGenerator = generator;
this.importBeanNameGenerator = generator;
}
}
}

if (this.environment == null) {
this.environment = new StandardEnvironment();
}

// Parse each @Configuration class
ConfigurationClassParser parser = new ConfigurationClassParser(
this.metadataReaderFactory, this.problemReporter, this.environment,
this.resourceLoader, this.componentScanBeanNameGenerator, registry);

Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());
// ↑ 这上面属于解析开始的准备工作 ↑
do {
parser.parse(candidates);
parser.validate();

Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
configClasses.removeAll(alreadyParsed);

// Read the model and create bean definitions based on its content
if (this.reader == null) {
this.reader = new ConfigurationClassBeanDefinitionReader(
registry, this.sourceExtractor, this.resourceLoader, this.environment,
this.importBeanNameGenerator, parser.getImportRegistry());
}
this.reader.loadBeanDefinitions(configClasses);
alreadyParsed.addAll(configClasses);

candidates.clear();
if (registry.getBeanDefinitionCount() > candidateNames.length) {
String[] newCandidateNames = registry.getBeanDefinitionNames();
Set<String> oldCandidateNames = new HashSet<>(Arrays.asList(candidateNames));
Set<String> alreadyParsedClasses = new HashSet<>();
for (ConfigurationClass configurationClass : alreadyParsed) {
alreadyParsedClasses.add(configurationClass.getMetadata().getClassName());
}
for (String candidateName : newCandidateNames) {
if (!oldCandidateNames.contains(candidateName)) {
BeanDefinition bd = registry.getBeanDefinition(candidateName);
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, this.metadataReaderFactory) &&
!alreadyParsedClasses.contains(bd.getBeanClassName())) {
candidates.add(new BeanDefinitionHolder(bd, candidateName));
}
}
}
candidateNames = newCandidateNames;
}
}
while (!candidates.isEmpty());

// Register the ImportRegistry as a bean in order to support ImportAware @Configuration classes
if (sbr != null && !sbr.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) {
sbr.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry());
}

if (this.metadataReaderFactory instanceof CachingMetadataReaderFactory) {
// Clear cache in externally provided MetadataReaderFactory; this is a no-op
// for a shared cache since it'll be cleared by the ApplicationContext.
((CachingMetadataReaderFactory) this.metadataReaderFactory).clearCache();
}
}

重点看看 parser.parse(candidates); 这句话是怎么解析的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public void parse(Set<BeanDefinitionHolder> configCandidates) {
for (BeanDefinitionHolder holder : configCandidates) {
BeanDefinition bd = holder.getBeanDefinition();
try {
if (bd instanceof AnnotatedBeanDefinition) {
// 因为这是一个使用注解的Bean,所以应该关注这里
parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
}
else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) {
parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName());
}
else {
parse(bd.getBeanClassName(), holder.getBeanName());
}
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex);
}
}

this.deferredImportSelectorHandler.process();
}

protected final void parse(AnnotationMetadata metadata, String beanName) throws IOException {
processConfigurationClass(new ConfigurationClass(metadata, beanName));
}

protected void processConfigurationClass(ConfigurationClass configClass) throws IOException {
// 如果当前的类是一个解析类,则跳过解析
if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {
return;
}

// 判断是否已经解析过了
ConfigurationClass existingClass = this.configurationClasses.get(configClass);
if (existingClass != null) {
if (configClass.isImported()) {
if (existingClass.isImported()) {
existingClass.mergeImportedBy(configClass);
}
// Otherwise ignore new imported config class; existing non-imported class overrides it.
return;
}
else {
// Explicit bean definition found, probably replacing an import.
// Let's remove the old one and go with the new one.
this.configurationClasses.remove(configClass);
this.knownSuperclasses.values().removeIf(configClass::equals);
}
}

// 递归解析配置类以及他的父类、接口等等
SourceClass sourceClass = asSourceClass(configClass);
do {
sourceClass = doProcessConfigurationClass(configClass, sourceClass);
}
while (sourceClass != null);

this.configurationClasses.put(configClass, configClass);
}

然后我们要进入 doProcessConfigurationClass 观察解析的过程。 这串代码其实有点长……:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
@Nullable
protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)
throws IOException {

if (configClass.getMetadata().isAnnotated(Component.class.getName())) {
// Recursively process any member (nested) classes first
processMemberClasses(configClass, sourceClass);
}

// 解析需要导入Property配置文件的类
for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), PropertySources.class,
org.springframework.context.annotation.PropertySource.class)) {
if (this.environment instanceof ConfigurableEnvironment) {
processPropertySource(propertySource);
}
else {
logger.info("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +
"]. Reason: Environment must implement ConfigurableEnvironment");
}
}

// 解析 @ComponentScan
Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
if (!componentScans.isEmpty() &&
!this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
for (AnnotationAttributes componentScan : componentScans) {
// The config class is annotated with @ComponentScan -> perform the scan immediately
Set<BeanDefinitionHolder> scannedBeanDefinitions =
this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
// Check the set of scanned definitions for any further config classes and parse recursively if needed
for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
if (bdCand == null) {
bdCand = holder.getBeanDefinition();
}
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
parse(bdCand.getBeanClassName(), holder.getBeanName());
}
}
}
}

// 解析 @Import 的类,这句话是重点
processImports(configClass, sourceClass, getImports(sourceClass), true);

// Process any @ImportResource annotations
AnnotationAttributes importResource =
AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class);
if (importResource != null) {
String[] resources = importResource.getStringArray("locations");
Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader");
for (String resource : resources) {
String resolvedResource = this.environment.resolveRequiredPlaceholders(resource);
configClass.addImportedResource(resolvedResource, readerClass);
}
}

// 解析 @Bean 方法
Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);
for (MethodMetadata methodMetadata : beanMethods) {
configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
}

// 解析接口默认方法
processInterfaces(configClass, sourceClass);

// 如果有父类,返回父类,由上一层继续调用这个方法继续解析
if (sourceClass.getMetadata().hasSuperClass()) {
String superclass = sourceClass.getMetadata().getSuperClassName();
if (superclass != null && !superclass.startsWith("java") &&
!this.knownSuperclasses.containsKey(superclass)) {
this.knownSuperclasses.put(superclass, configClass);
// Superclass found, return its annotation metadata and recurse
return sourceClass.getSuperClass();
}
}

// 返回Null表示解析结束
return null;
}

好了,可以看到通过 Java 类的解析可谓覆盖到尼玛我想象不到的地方….但他们就是应该要有 挑重点来看吧,我的主类没有父级也没有接口,直接重点看 processImports(configClass, sourceClass, getImports(sourceClass), true) 这句话。 这句话首先需要获取所有的导入配置 getImports(sourceClass)

1
2
3
4
5
6
private Set<SourceClass> getImports(SourceClass sourceClass) throws IOException {
Set<SourceClass> imports = new LinkedHashSet<>();
Set<SourceClass> visited = new LinkedHashSet<>();
collectImports(sourceClass, imports, visited);
return imports;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
private void collectImports(SourceClass sourceClass, Set<SourceClass> imports, Set<SourceClass> visited)
throws IOException {

if (visited.add(sourceClass)) {
for (SourceClass annotation : sourceClass.getAnnotations()) {
String annName = annotation.getMetadata().getClassName();
if (!annName.equals(Import.class.getName())) {
collectImports(annotation, imports, visited);
}
}
imports.addAll(sourceClass.getAnnotationAttributes(Import.class.getName(), "value"));
}
}

这两个东西,切忌不要去 debug 一个一个看….我在这里看晕了好几个小时,应该是条件断点判断 sourceClass.getAnnotationAttributes(Import.class.getName(), "value").size() > 0 再停住

好了等到断点停住的时候,发现是 @interface SpringBootApplication > @interface EnableAutoConfiguration 上面的 @Import(AutoConfigurationImportSelector.class) 在这里导入了。以及这个类上边的 @AutoConfigurationPackage 导入了 @Import(AutoConfigurationPackages.Registrar.class) 所以当前需要解析的 importCandidatesAutoConfigurationPackages.Registrar.class 以及 AutoConfigurationImportSelector.class 两个类。 那么这两个类是什么用的,我现在也还不知道…先知道他被导入就好了,接下去看 processImports 方法了,这个方法应该有答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
Collection<SourceClass> importCandidates, boolean checkForCircularImports) {

if (importCandidates.isEmpty()) {
return;
}

// 由上面传递的 checkForCircularImports 参数决定是否判断有没有循环引入
if (checkForCircularImports && isChainedImportOnStack(configClass)) {
this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
}
else {
this.importStack.push(configClass);
try {
// 开始解析导入类
for (SourceClass candidate : importCandidates) {
// 第一层,根据不同的导入类型进行解析
// AutoConfigurationImportSelector 来到这个分支
if (candidate.isAssignable(ImportSelector.class)) {
// Candidate class is an ImportSelector -> delegate to it to determine imports
Class<?> candidateClass = candidate.loadClass();
ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class,
this.environment, this.resourceLoader, this.registry);
if (selector instanceof DeferredImportSelector) {
this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
}
else {
String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames);
// 递归解析
processImports(configClass, currentSourceClass, importSourceClasses, false);
}
}
// AutoConfigurationPackages.Registrar进入这个分支
else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
// Candidate class is an ImportBeanDefinitionRegistrar ->
// delegate to it to register additional bean definitions
Class<?> candidateClass = candidate.loadClass();
ImportBeanDefinitionRegistrar registrar =
ParserStrategyUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class,
this.environment, this.resourceLoader, this.registry);
configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
}
else {
// 直接以@Configuration的方式进行处理
this.importStack.registerImport(
currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
processConfigurationClass(candidate.asConfigClass(configClass));
}
}
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to process import candidates for configuration class [" +
configClass.getMetadata().getClassName() + "]", ex);
}
finally {
this.importStack.pop();
}
}
}

不行呀,因为需要根据类型来做,先给两个类的类型吧:

这三个分支对应着三个不同的配置类型:

  1. ImportSelector 用于导入配置类的信息
  2. Registrar 用于解析自定义注解动态生成 Bean 的信息
  3. @Configuration 读取到配置类,直接解析

那我们就先按照上图的顺序来解析这些配置类吧,首先是 AutoConfigurationPackages.Registrar 对象的解析。我先从上面一大串截取代码片段来看:

1
2
3
4
5
6
7
8
9
10
11
// ...
else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
// 额外注册BeanDefinition用
Class<?> candidateClass = candidate.loadClass();
ImportBeanDefinitionRegistrar registrar =
ParserStrategyUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class,
this.environment, this.resourceLoader, this.registry);
// 简单的添加(导入)到当前的 configClass 中
configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
}
// ...

先放到后面,应该是解析的时候会调用到。 接下来看第二个 AutoConfigurationImportSelector.class 的解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (candidate.isAssignable(ImportSelector.class)) {
// Candidate class is an ImportSelector -> delegate to it to determine imports
Class<?> candidateClass = candidate.loadClass();
ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class,
this.environment, this.resourceLoader, this.registry);
if (selector instanceof DeferredImportSelector) {
// 进入这里
this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
}
else {
String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames);
processImports(configClass, currentSourceClass, importSourceClasses, false);
}
}

这一块比较麻烦,在上面 uml 可以看到,AutoConfigurationImportSelector.class 属于 DeferredImportSelector,所以交给 deferredImportSelectorHandler 进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
public void handle(ConfigurationClass configClass, DeferredImportSelector importSelector) {
DeferredImportSelectorHolder holder = new DeferredImportSelectorHolder(
configClass, importSelector);
if (this.deferredImportSelectors == null) {
DeferredImportSelectorGroupingHandler handler = new DeferredImportSelectorGroupingHandler();
handler.register(holder);
handler.processGroupImports();
}
else {
// 来到这里,简单的加入
this.deferredImportSelectors.add(holder);
}
}

导入刚刚注册的配置

上面一大堆全是解析,然后接下来就要处理这些自动配置了。 回到 parser.parse(candidates); 这个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public void parse(Set<BeanDefinitionHolder> configCandidates) {
for (BeanDefinitionHolder holder : configCandidates) {
BeanDefinition bd = holder.getBeanDefinition();
try {
if (bd instanceof AnnotatedBeanDefinition) {
parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
}
else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) {
parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName());
}
else {
parse(bd.getBeanClassName(), holder.getBeanName());
}
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex);
}
}

// 处理导入配置类(第三方配置)
this.deferredImportSelectorHandler.process();
}

AutoConfigurationImportSelector解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void process() {
// AutoConfigurationImportSelector集合 只有一个元素
List<DeferredImportSelectorHolder> deferredImports = this.deferredImportSelectors;
this.deferredImportSelectors = null;
try {
if (deferredImports != null) {
DeferredImportSelectorGroupingHandler handler = new DeferredImportSelectorGroupingHandler();
deferredImports.sort(DEFERRED_IMPORT_COMPARATOR);
// 注册到 DeferredImportSelectorGroupingHandler 中
deferredImports.forEach(handler::register);
// 重点看这边
handler.processGroupImports();
}
}
finally {
this.deferredImportSelectors = new ArrayList<>();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void processGroupImports() {
for (DeferredImportSelectorGrouping grouping : this.groupings.values()) {
grouping.getImports().forEach(entry -> {
ConfigurationClass configurationClass = this.configurationClasses.get(
entry.getMetadata());
try {
processImports(configurationClass, asSourceClass(configurationClass),
asSourceClasses(entry.getImportClassName()), false);
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to process import candidates for configuration class [" +
configurationClass.getMetadata().getClassName() + "]", ex);
}
});
}
}

先进入这里 grouping.getImports()

1
2
3
4
5
6
7
public Iterable<Group.Entry> getImports() {
for (DeferredImportSelectorHolder deferredImport : this.deferredImports) {
this.group.process(deferredImport.getConfigurationClass().getMetadata(),
deferredImport.getImportSelector());
}
return this.group.selectImports();
}

这个过程就是 SpringBoot 提供的,在类路径提供 additional-spring-configuration-metadata.json 提供导入配置的,然而看起来好像所有的 SpringBoot 模块都在这里了,他只是通过导入类路径的包来判断是否要加载配置。 跳过这一段吧,看着有点累。我们现在只要知道,读取了类路径下的 additional-spring-configuration-metadata.json 后,我们想要看到的 MybatisAutoConfiguration 已经被读取到了就好了。

同样的,spring-boot-mybatis-starter 也提供了上面的注册文件:

好了,接下来回到上面的解析方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void processGroupImports() {
for (DeferredImportSelectorGrouping grouping : this.groupings.values()) {
grouping.getImports().forEach(entry -> {
ConfigurationClass configurationClass = this.configurationClasses.get(
entry.getMetadata());
try {
// 此时进入processImports的时候就不需要再导入配置了
// 直接以 @Configuration 去实现配置
processImports(configurationClass, asSourceClass(configurationClass),
asSourceClasses(entry.getImportClassName()), false);
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to process import candidates for configuration class [" +
configurationClass.getMetadata().getClassName() + "]", ex);
}
});
}
}

那么关于 @Configuration 的解析,在我 之前的文章 就可以看到了。 现在就看看怎么根据主类的所在的包,扫描该包下的所有 Bean

自动包扫描注册Bean

我们得回到 doProcessConfigurationClass 的这个地方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 解析 @ComponentScan,会从主类 WebTestApplication 中拿到 BasePackage
// 然后根据这个 BasePackage 的配置进行扫描下面的类
Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
if (!componentScans.isEmpty() &&
!this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
for (AnnotationAttributes componentScan : componentScans) {
// 通过 componentScanParser 进行解析
Set<BeanDefinitionHolder> scannedBeanDefinitions =
this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
// Check the set of scanned definitions for any further config classes and parse recursively if needed
for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
if (bdCand == null) {
bdCand = holder.getBeanDefinition();
}
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
parse(bdCand.getBeanClassName(), holder.getBeanName());
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, final String declaringClass) {
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(this.registry,
componentScan.getBoolean("useDefaultFilters"), this.environment, this.resourceLoader);

Class<? extends BeanNameGenerator> generatorClass = componentScan.getClass("nameGenerator");
boolean useInheritedGenerator = (BeanNameGenerator.class == generatorClass);
scanner.setBeanNameGenerator(useInheritedGenerator ? this.beanNameGenerator :
BeanUtils.instantiateClass(generatorClass));

ScopedProxyMode scopedProxyMode = componentScan.getEnum("scopedProxy");
if (scopedProxyMode != ScopedProxyMode.DEFAULT) {
scanner.setScopedProxyMode(scopedProxyMode);
}
else {
Class<? extends ScopeMetadataResolver> resolverClass = componentScan.getClass("scopeResolver");
scanner.setScopeMetadataResolver(BeanUtils.instantiateClass(resolverClass));
}

scanner.setResourcePattern(componentScan.getString("resourcePattern"));

for (AnnotationAttributes filter : componentScan.getAnnotationArray("includeFilters")) {
for (TypeFilter typeFilter : typeFiltersFor(filter)) {
scanner.addIncludeFilter(typeFilter);
}
}
for (AnnotationAttributes filter : componentScan.getAnnotationArray("excludeFilters")) {
for (TypeFilter typeFilter : typeFiltersFor(filter)) {
scanner.addExcludeFilter(typeFilter);
}
}

boolean lazyInit = componentScan.getBoolean("lazyInit");
if (lazyInit) {
scanner.getBeanDefinitionDefaults().setLazyInit(true);
}

Set<String> basePackages = new LinkedHashSet<>();
String[] basePackagesArray = componentScan.getStringArray("basePackages");
for (String pkg : basePackagesArray) {
String[] tokenized = StringUtils.tokenizeToStringArray(this.environment.resolvePlaceholders(pkg),
ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
Collections.addAll(basePackages, tokenized);
}
for (Class<?> clazz : componentScan.getClassArray("basePackageClasses")) {
basePackages.add(ClassUtils.getPackageName(clazz));
}
if (basePackages.isEmpty()) {
// 没有指定扫描包,进入这里获取主类的包名
basePackages.add(ClassUtils.getPackageName(declaringClass));
}

scanner.addExcludeFilter(new AbstractTypeHierarchyTraversingFilter(false, false) {
@Override
protected boolean matchClassName(String className) {
return declaringClass.equals(className);
}
});

// 上面基本上都是解析配置的一些东西,过滤器啊,LazyInit等等,到这一步才是真正的扫描
// StringUtils.toStringArray(basePackages) 获取包名
return scanner.doScan(StringUtils.toStringArray(basePackages));
}

真正的扫描,做的事情可谓多得多

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
Assert.notEmpty(basePackages, "At least one base package must be specified");
Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
for (String basePackage : basePackages) {
// 这里又是一个大的模块了,也就是读取ClassPath下所有的文件
Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
// 拿到扫描到的 BeanDefinition 注册到工厂中
for (BeanDefinition candidate : candidates) {
ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
candidate.setScope(scopeMetadata.getScopeName());
String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
if (candidate instanceof AbstractBeanDefinition) {
postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
}
if (candidate instanceof AnnotatedBeanDefinition) {
AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
}
if (checkCandidate(beanName, candidate)) {
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
definitionHolder =
AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
beanDefinitions.add(definitionHolder);
registerBeanDefinition(definitionHolder, this.registry);
}
}
}
return beanDefinitions;
}

扫描:

1
2
3
4
5
6
7
8
public Set<BeanDefinition> findCandidateComponents(String basePackage) {
if (this.componentsIndex != null && indexSupportsIncludeFilters()) {
return addCandidateComponentsFromIndex(this.componentsIndex, basePackage);
}
else {
return scanCandidateComponents(basePackage);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
Set<BeanDefinition> candidates = new LinkedHashSet<>();
try {
String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
resolveBasePackage(basePackage) + '/' + this.resourcePattern;
Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
boolean traceEnabled = logger.isTraceEnabled();
boolean debugEnabled = logger.isDebugEnabled();
// 拿到包下两个文件的 Resource 实例
for (Resource resource : resources) {
if (traceEnabled) {
logger.trace("Scanning " + resource);
}
if (resource.isReadable()) {
try {
MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
if (isCandidateComponent(metadataReader)) {
ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
sbd.setResource(resource);
sbd.setSource(resource);
// 符合某种条件,暂且就当BF中还没存在这个Bean吧
if (isCandidateComponent(sbd)) {
if (debugEnabled) {
logger.debug("Identified candidate component class: " + resource);
}
// 添加
candidates.add(sbd);
}
else {
if (debugEnabled) {
logger.debug("Ignored because not a concrete top-level class: " + resource);
}
}
}
else {
if (traceEnabled) {
logger.trace("Ignored because not matching any filter: " + resource);
}
}
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to read candidate component class: " + resource, ex);
}
}
else {
if (traceEnabled) {
logger.trace("Ignored because not readable: " + resource);
}
}
}
}
catch (IOException ex) {
throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex);
}
// 返回
return candidates;
}

OK,扫描完成,后在 finishBeanFactoryInitialization 进行初始化就可以了

结束

SpringBoot

OK,现代 Java 开发者应该都对 SpringBoot 很熟悉了吧,一个很 "轻量级"IOC 容器。 我记得 SpringBoot 刚出来的时候,很多博客文章都会说 SpringBoot 减轻了 Java 开发工作者的负担,是个轻量级的框架。然而后面我才发现,并不轻量级。因为,SpringBoot 把需要依赖的东西给封装了起来,但其实比起之前自己控制依赖项目来说,反而会更重了一些,毕竟以前还可以自由组合。现在,SpringBoot 以及框架作者都提供了默认的依赖以及默认的配置,所以说这个框架轻量级其实并不是,要说轻量级应该只是说开发轻量级而已,菜鸟也可以快速上手建立一个后台项目而不必去关心太多项目配置的东西。 框架肯定是一个优秀的框架,我们项目全体也都是 SpringBoot 架构起来的,所以还是需要看看,SpringBoot 偷偷帮我们做了什么事情。

依赖简化

日常使用中,我们只需要引入一个 starter,就可以神奇把我们整合的框架整合起来。但是在以前,我们要使用 MyBatis 的时候,却需要引入 org.mybatis:mybatis 主框架,然后还因为需要整合 Spring 框架,所以我们还需要引入一个 org.mybatis:mybatis-spring。这就算了,这两个包如果版本号对不上,还要出现兼容性的问题。 然而现在我们只需要一个 org.mybatis.spring.boot:mybatis-spring-boot-starter,就可以同时引入这两个包,而且版本号还解决的很OK。所以现在我们可以看看他的 pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot</artifactId>
<version>1.3.1</version>
</parent>
<artifactId>mybatis-spring-boot-starter</artifactId>
<name>mybatis-spring-boot-starter</name>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
</dependency>
</dependencies>
</project>

所以结论是,一个 starter,定义了需要依赖的包的版本,然后通过依赖传递将这些包传递到我们 starter 所在的项目上来。 目前我建立了一个项目,这个项目很简单,只是依赖了一个 spring-boot-starter-web, 我们可以在 idea 上很方便的查看依赖的所有东西:

但其实这个做法在以前 Spring 的时候是已经存在了,这个项目就是 Spring-IO,但我也不知道为什么,可能因为名字取得不够好吧?然后这个项目好像很少人用。或者以前的人觉得会被依赖很多东西进来所以不用这个项目了?

自动装配

我们项目所需要的依赖都弄进来了,下一步就是配置让这些包可以相互配合,共同提供服务呀。 所以,SpringBoot 所提供的第二个功能就是,根据默认的配置,装配依赖进来包里面的 Bean 实例。依赖+装配,就是我们以前一直所需要的操作了,可以说这个框架减少了全世界程序员按 CTRL+C/V 的次数hhhhh。 OK,简单了解的话,我需要一个额外的 starter,这里就使用 org.mybatis.spring.boot:mybatis-spring-boot-starter 来看,仓库地址 项目的目录结构为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
├── LICENSE
├── README.md
├── license.txt
├── mvnw
├── mvnw.cmd
├── mybatis-spring-boot-autoconfigure
│   ├── format.xml
│   ├── license.txt
│   ├── pom.xml
│   └── src
├── mybatis-spring-boot-samples
│   ├── mybatis-spring-boot-sample-annotation
│   ├── mybatis-spring-boot-sample-freemarker
│   ├── mybatis-spring-boot-sample-freemarker-legacy
│   ├── mybatis-spring-boot-sample-groovy
│   ├── mybatis-spring-boot-sample-kotlin
│   ├── mybatis-spring-boot-sample-thymeleaf
│   ├── mybatis-spring-boot-sample-velocity
│   ├── mybatis-spring-boot-sample-velocity-legacy
│   ├── mybatis-spring-boot-sample-war
│   ├── mybatis-spring-boot-sample-web
│   ├── mybatis-spring-boot-sample-xml
│   └── pom.xml
├── mybatis-spring-boot-starter
│   ├── license.txt
│   └── pom.xml
├── mybatis-spring-boot-starter-test
│   └── pom.xml
├── mybatis-spring-boot-test-autoconfigure
│   ├── format.xml
│   ├── pom.xml
│   └── src
├── pom.xml
└── travis
├── after_success.sh
└── settings.xml

其中,mybatis-spring-boot-autoconfigure 比较惹人注目,所以我们现在就看看这个项目。 首先我们看看 main/resources/META-INF/spring.factories,因为 Spring 很喜欢通过这些 meta 文件来促使各个模块很好的解耦但又能彼此配合工作,所以这个文件是定义 自动装配 开始的工厂类:

1
2
3
4
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration,\
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration

那么通过看初步的源码,这两个类都是熟悉的 Java 装配类,那么将会被加入到前面提到的 BeanFactory 容器中,后面解析将会调用里面的配置方法。 第二个优秀的地方就是,定义配置类,就可以在 yaml 文件中提示的出现,可以利用 IDE 工具很好的防止配置名写错。这个类就是 MybatisProperties.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@ConfigurationProperties(prefix = MybatisProperties.MYBATIS_PREFIX)
public class MybatisProperties {

public static final String MYBATIS_PREFIX = "mybatis";

private static final ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();

/**
* Location of MyBatis xml config file.
*/
private String configLocation;

/**
* Locations of MyBatis mapper files.
*/
private String[] mapperLocations;

...
}

OK,我们可以看到,定义了前缀就是 mybatis,而这个类是个贫血型的 Bean,只有属性。这些属性将会贯穿 MyBatis 在项目中整个生命周期。 接下来一个问题,我并不需要每个属性都在 yaml 文件中去定义啊,有些直接使用官方提供的默认值就可以了。 所以官方又贴心的提供了 additional-spring-configuration-metadata.json 这个 JSON 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
{
"properties": [
{
"sourceType": "org.apache.ibatis.session.Configuration",
"defaultValue": "org.apache.ibatis.scripting.xmltags.XMLLanguageDriver",
"name": "mybatis.configuration.default-scripting-language",
"description": "A default LanguageDriver class.",
"type": "java.lang.Class<? extends org.apache.ibatis.scripting.LanguageDriver>",
"deprecation": {
"reason": "Because when this configuration property is used, there is case that custom language driver cannot be registered correctly.",
"replacement": "mybatis.default-scripting-language-driver"
}
},
{
"sourceType": "org.apache.ibatis.session.Configuration",
"defaultValue": "org.apache.ibatis.type.EnumTypeHandler",
"name": "mybatis.configuration.default-enum-type-handler",
"description": "A default TypeHandler class for Enum.",
"type": "java.lang.Class<? extends org.apache.ibatis.type.TypeHandler>"
},
{
"defaultValue": false,
"name": "mybatis.lazy-initialization",
"description": "Set whether enable lazy initialization for mapper bean.",
"type": "java.lang.Boolean"
},
{
"name": "mybatis.scripting-language-driver.velocity.userdirective",
"deprecation": {
"level": "error",
"reason": "The 'userdirective' is deprecated since Velocity 2.x. This property defined for keeping backward compatibility with older velocity version.",
"replacement": "mybatis.scripting-language-driver.velocity.velocity-settings.runtime.custom_directives"
}
}

]
}

不好理解的话,我们直接看第三个默认配置就好了:

1
2
3
4
5
6
{
"defaultValue": false,
"name": "mybatis.lazy-initialization",
"description": "Set whether enable lazy initialization for mapper bean.",
"type": "java.lang.Boolean"
}

看了下图应该明白了吧,上面那个文件就是定义配置的一些说明、默认值的。

所以当我们要造一个框架,又因为很多约定的东西,靠人脑来记已经靠不住的情况下,就可以编写类似于 additional-spring-configuration-metadata.json 这种文件来做约定以及说明了。


OK,上面说完了 SpringBoot 的两个最主要的优点以后,现在就来看看源码。但是源码这块,因为 传递依赖 利用的是 mvn/gradle 的特性,所以 依赖传递 并不需要再说。 那么最主要的就是来解析,怎么自动装配的问题。

简单的SpringBoot项目

pom.xml 依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.BUILD-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.liweidan.web</groupId>
<artifactId>web-test</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>web-test</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

一个主启动器:

1
2
3
4
5
6
7
8
@SpringBootApplication
public class WebTestApplication {

public static void main(String[] args) {
SpringApplication.run(WebTestApplication.class, args);
}

}

一个控制器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
public class UserController {

@GetMapping("users")
public List<Map<String, String>> users() {
List<Map<String, String>> users = new ArrayList<>();
for (int i = 0; i < 3; i++) {
Map<String, String> u = new HashMap<>();
u.put("id", String.valueOf(i));
u.put("name", "name" + i);
users.add(u);
}
return users;
}

}

SpringApplication元信息准备

写过 SpringBoot 项目的同学应该对这个很熟悉了,通过传递一个上下文的根类,SpringBoot 将会自动装载在此类所在的包下面的所有类,并且 args 很明显就是我们在控制台传递的参数,也一并传递给 SpringApplication.run 这个方法,SpringBoot 即可将命令行的参数配置覆盖配置文件指定的配置。 接下来看看这个类做了什么事情:

1
2
3
4
5
6
7
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
return run(new Class<?>[] { primarySource }, args);
}

public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
return new SpringApplication(primarySources).run(args);
}

通过静态方法,传递一个 Class 类作为主资源,然后再传递给主资源数组的 run 方法。那么这个数组其实我们是可以传递多个主资源的,比如我们做项目的时候,想要每个模块包彼此分离,即可传递多个包的主资源路径。 new SpringApplication(primarySources).run(args); 才是真正的进入容器的准备阶段: 首先看看构造器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
// 初始化主资源链表,用于下面需要读取的时候可以遍历.
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
// 判断Web环境,有webFlux/web/普通三个环境,主要通过类路径是否带有相对应需要的类来判断,如果都没有则初始化为普通Java项目.
this.webApplicationType = WebApplicationType.deduceFromClasspath();
// 初始化环境,这个接口多用于web环境,因为需要从web上下文加载一些信息.
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
// 初始化监听器,监听容器生命周期中需要回调的函数
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
// 从运行堆栈中寻找运行这个类的main方法所在的类
this.mainApplicationClass = deduceMainApplicationClass();
}

加载不同模块的元信息

在上面的构造器中看到 setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));Spring 还是跟以前获取 handlers 一样。通过在 META-INF 类加载路径定义不同的 spring.factories,使用类加载器读取这些配置文件资源,解析,加载配置文件中的类,初始化对象,来共同完成业务。

1
2
3
4
5
6
7
8
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
ClassLoader classLoader = getClassLoader();
// Use names and ensure unique to protect against duplicates
Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
AnnotationAwareOrderComparator.sort(instances);
return instances;
}

通过 SpringFactoriesLoader.loadFactoryNames(type, classLoader),加载当前 ClassLoader 中的指定类,源码显示如何读取,后面的 getOrDefault(factoryTypeName, Collections.emptyList()) 还是比较好理解的,如果加载到工厂类的名字就返回不然返回个空集合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
String factoryTypeName = factoryType.getName();
return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
}

private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
// 相当于Map<String, List<String>>结构,先从缓存命中.
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null) {
return result;
}

/*
属性定义了元信息的路径以及名字
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
*/
try {
// 开始加载类路径下特定名字的元信息文件
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
result = new LinkedMultiValueMap<>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
// 通过UrlResource解析成Properties对象
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
// 遍历所有模块的指定文件,然后加入缓存
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryTypeName = ((String) entry.getKey()).trim();
for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
result.add(factoryTypeName, factoryImplementationName.trim());
}
}
}
cache.put(classLoader, result);
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}

启动项目SpringApplication.run

可以看到 run 的源码跟之前的 AbstractApplicationContext#refresh 的味道还是一样的,先加载一系列容器运行时需要的生命周期类(Spring 模块间的也有用户自定义的),然后 refreshContext(context) 刷新容器上下文。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public ConfigurableApplicationContext run(String... args) {
// 这是用来打印加载时间的工具类,暂时可以略过.
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
// 主要设置JVM虚拟机支持无设备情况下让awt可运行的属性
configureHeadlessProperty();
// 一. 加载所有SpringApplicationRunListeners并开始遍历所有Lintener启动监听回调函数
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// 二. 开始准备ConfigurableEnvironment环境
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
// 三. 创建应用上下文
context = createApplicationContext();
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
// 四. 做一些准备工作
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
// 五. 刷新上下文
refreshContext(context);
// 六. 刷新完成后做的一些清理、回调工作
afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
// 七. 启动完成后,调用所有SpringApplicationRunListener的完成启动的回调函数
listeners.started(context);
// 八. 主要处理ApplicationRunner和CommandLineRunner的回调
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}

try {
// 九. 运行时的SpringApplicationRunListener回调函数
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
return context;
}

装载SpringApplicationRunListener

1
2
3
4
5
private SpringApplicationRunListeners getRunListeners(String[] args) {
Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
return new SpringApplicationRunListeners(logger,
getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args));
}

首先我们看第一步,加载所有的 SpringApplicationRunListener 子类,这个加载方式跟上面所说的加载元信息是一样的,只不过指定加载 SpringApplicationRunListener.class 类。 我们知道,Spring 经常会定义很多生命周期回调,供用户根据需求切入框架。这次 SpringBoot 的生命周期是 SpringApplicationRunListener。 首先看看 SpringApplicationRunListener 定义哪些生命周期函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public interface SpringApplicationRunListener {

// 项目开始时调用
default void starting() {
}

// 环境准备好时
default void environmentPrepared(ConfigurableEnvironment environment) {
}

// 上下文准备好时
default void contextPrepared(ConfigurableApplicationContext context) {
}

// 上下文读取完成
default void contextLoaded(ConfigurableApplicationContext context) {
}

// 启动完成
default void started(ConfigurableApplicationContext context) {
}

// 项目运行时调用
default void running(ConfigurableApplicationContext context) {
}

// 项目失败时
default void failed(ConfigurableApplicationContext context, Throwable exception) {
}

}

准备ConfigurableEnvironment环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments) {
// 首先根据环境类型获取对象的配置实现对象
ConfigurableEnvironment environment = getOrCreateEnvironment();
// 配置ConversionService单例对象,以及判断有没有设置Profiles环境
configureEnvironment(environment, applicationArguments.getSourceArgs());
// 读取其他环境中(比如web.xml)读取的配置属性
ConfigurationPropertySources.attach(environment);
// 继续上面的SpringApplicationRunListener生命周期调用
listeners.environmentPrepared(environment);
// 把环境绑定到Binder中,Binder是Spring提供的一个记录对象的容器
bindToSpringApplication(environment);
if (!this.isCustomEnvironment) {
// 如果当前的配置环境和重新判断的环境不同,则转换成当前的环境(有可能在生命周期修改了加载的环境?)
environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
deduceEnvironmentClass());
}
// 重新读取特定环境的配置
ConfigurationPropertySources.attach(environment);
return environment;
}

private ConfigurableEnvironment getOrCreateEnvironment() {
if (this.environment != null) {
return this.environment;
}
switch (this.webApplicationType) {
case SERVLET:
return new StandardServletEnvironment();
case REACTIVE:
return new StandardReactiveWebEnvironment();
default:
return new StandardEnvironment();
}
}

然后在主线程中配置忽略 BooleanBean

1
2
3
4
5
6
7
8
9
10
11
12
13
public ConfigurableApplicationContext run(String... args) {
//...
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);
//...
}

private void configureIgnoreBeanInfo(ConfigurableEnvironment environment) {
if (System.getProperty(CachedIntrospectionResults.IGNORE_BEANINFO_PROPERTY_NAME) == null) {
Boolean ignore = environment.getProperty("spring.beaninfo.ignore", Boolean.class, Boolean.TRUE);
System.setProperty(CachedIntrospectionResults.IGNORE_BEANINFO_PROPERTY_NAME, ignore.toString());
}
}

创建上下文

1
2
3
4
context = createApplicationContext();
// 获取模块的错误解释器,用于项目启动的时候解析是什么错误
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);

根据不同环境创建不同的上下文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
protected ConfigurableApplicationContext createApplicationContext() {
Class<?> contextClass = this.applicationContextClass;
if (contextClass == null) {
try {
switch (this.webApplicationType) {
case SERVLET:
contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);
break;
case REACTIVE:
contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
break;
default:
contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
}
}
catch (ClassNotFoundException ex) {
throw new IllegalStateException(
"Unable create a default ApplicationContext, please specify an ApplicationContextClass", ex);
}
}
return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
}

Context准备工作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment,
SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
// 设置上面准备好的环境信息
context.setEnvironment(environment);
// 处理之前做一些事情,但是当前环境下,只是注册了一个 ConversionService 到 BeanFactory 中
postProcessApplicationContext(context);
// 调用之前注册的所有Initializers
applyInitializers(context);
// 再调用所有的listeners
listeners.contextPrepared(context);
if (this.logStartupInfo) {
logStartupInfo(context.getParent() == null);
logStartupProfileInfo(context);
}
// 注册SpringBoot独有的一些Bean
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
if (printedBanner != null) {
beanFactory.registerSingleton("springBootBanner", printedBanner);
}
// 接下来跟之前一样
if (beanFactory instanceof DefaultListableBeanFactory) {
((DefaultListableBeanFactory) beanFactory)
.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
}
if (this.lazyInitialization) {
context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
}
// Load the sources
Set<Object> sources = getAllSources();
Assert.notEmpty(sources, "Sources must not be empty");
// 根据我们传递的主类进行读取,放入BeanFactory中
load(context, sources.toArray(new Object[0]));
listeners.contextLoaded(context);
}

其实当前 postProcessApplicationContext 除了注册 ConversionService 以外其他事情都没做。

refreshContext刷新上下文

因为我要看看自动装配的问题,所以这个时候为了故事比较好说,我加了 mybatis-spring-boot-starter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.BUILD-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.liweidan.web</groupId>
<artifactId>web-test</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>web-test</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.197</version>
</dependency>

</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

刷新上下文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void refreshContext(ConfigurableApplicationContext context) {
refresh(context);
if (this.registerShutdownHook) {
try {
context.registerShutdownHook();
}
catch (AccessControlException ex) {
// Not allowed in some environments.
}
}
}
protected void refresh(ApplicationContext applicationContext) {
Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext);
((AbstractApplicationContext) applicationContext).refresh();
}

然后接下来就去到我们熟悉的 AbstractApplicationContext 中,重温一下这个 refresh() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
prepareRefresh();

// Tell the subclass to refresh the internal bean factory.
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);

try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);

// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);

// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);

// Initialize message source for this context.
initMessageSource();

// Initialize event multicaster for this context.
initApplicationEventMulticaster();

// Initialize other special beans in specific context subclasses.
onRefresh();

// Check for listener beans and register them.
registerListeners();

// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);

// Last step: publish corresponding event.
finishRefresh();
}

catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}

// Destroy already created singletons to avoid dangling resources.
destroyBeans();

// Reset 'active' flag.
cancelRefresh(ex);

// Propagate exception to caller.
throw ex;
}

finally {
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
resetCommonCaches();
}
}
}

同之前一样,在 invokeBeanFactoryPostProcessors(beanFactory) 这句话出现了 BeanDefinition 数量的剧增,那么我们可以推断出,这句话还是关键,就是在这句话开始解析我们的项目依赖。

其实按照我的猜测,自动配置应该是 starter 提供了一些配置类交给 SpringBoot 注入 Spring 的 BeanFactory 中,但是现在看来,好像 SpringBoot 就不需要做什么东西,直接委托给容器扫描解析执行了。

invokeBeanFactoryPostProcessors

1
2
3
4
5
6
7
8
9
10
11
protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
// 利用委派模式委托给 PostProcessorRegistrationDelegate 进行执行
PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());

// Detect a LoadTimeWeaver and prepare for weaving, if found in the meantime
// (e.g. through an @Bean method registered by ConfigurationClassPostProcessor)
if (beanFactory.getTempClassLoader() == null && beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) {
beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));
beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
}
}

PostProcessorRegistrationDelegate委派对象

这是一个在 Context 模块下对象,主要处理 PostProcessor 相关的事情,看到签名只提供两个方法:

  • invokeBeanFactoryPostProcessors 执行 BF 的 PostProcessors
  • registerBeanPostProcessors 注册 PostProcessors 到 BF 中

关于普通的配置类解析,之前在 chapters3_高级的Beafctory—Spring上下文 已经有提到,解析项目中配置类不明白的话可以先看看那篇文章。 不过我们现在重点是怎么读取到 starter 框架里边的自动配置信息:

1
2
3
4
5
6
7
private static void invokeBeanDefinitionRegistryPostProcessors(
Collection<? extends BeanDefinitionRegistryPostProcessor> postProcessors, BeanDefinitionRegistry registry) {

for (BeanDefinitionRegistryPostProcessor postProcessor : postProcessors) {
postProcessor.postProcessBeanDefinitionRegistry(registry);
}
}

当前 postProcessors 里边只有一个 ConfigurationClassPostProcessor 那我们进入对应的 postProcessBeanDefinitionRegistry 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
int registryId = System.identityHashCode(registry);
if (this.registriesPostProcessed.contains(registryId)) {
throw new IllegalStateException(
"postProcessBeanDefinitionRegistry already called on this post-processor against " + registry);
}
if (this.factoriesPostProcessed.contains(registryId)) {
throw new IllegalStateException(
"postProcessBeanFactory already called on this post-processor against " + registry);
}
this.registriesPostProcessed.add(registryId);

processConfigBeanDefinitions(registry);
}

processConfigBeanDefinitions 解析配置

由于我们现在需要跟自动导入的配置,所以我们应该需要跟的类配置是我们的主类,也就是 WebTestApplication 这个类,这个类上修饰的注解 @SpringBootApplication 是自动注入的关键

这个才是执行自动配置的重点之重:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
/**
* Build and validate a configuration model based on the registry of
* {@link Configuration} classes.
*/
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
List<BeanDefinitionHolder> configCandidates = new ArrayList<>();
String[] candidateNames = registry.getBeanDefinitionNames();

for (String beanName : candidateNames) {
BeanDefinition beanDef = registry.getBeanDefinition(beanName);
if (beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE) != null) {
if (logger.isDebugEnabled()) {
logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);
}
}
else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
}
}

// Return immediately if no @Configuration classes were found
if (configCandidates.isEmpty()) {
return;
}

// Sort by previously determined @Order value, if applicable
configCandidates.sort((bd1, bd2) -> {
int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition());
int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition());
return Integer.compare(i1, i2);
});

// Detect any custom bean name generation strategy supplied through the enclosing application context
SingletonBeanRegistry sbr = null;
if (registry instanceof SingletonBeanRegistry) {
sbr = (SingletonBeanRegistry) registry;
if (!this.localBeanNameGeneratorSet) {
BeanNameGenerator generator = (BeanNameGenerator) sbr.getSingleton(
AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR);
if (generator != null) {
this.componentScanBeanNameGenerator = generator;
this.importBeanNameGenerator = generator;
}
}
}

if (this.environment == null) {
this.environment = new StandardEnvironment();
}

// Parse each @Configuration class
ConfigurationClassParser parser = new ConfigurationClassParser(
this.metadataReaderFactory, this.problemReporter, this.environment,
this.resourceLoader, this.componentScanBeanNameGenerator, registry);

Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());
// ↑ 这上面属于解析开始的准备工作 ↑
do {
parser.parse(candidates);
parser.validate();

Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
configClasses.removeAll(alreadyParsed);

// Read the model and create bean definitions based on its content
if (this.reader == null) {
this.reader = new ConfigurationClassBeanDefinitionReader(
registry, this.sourceExtractor, this.resourceLoader, this.environment,
this.importBeanNameGenerator, parser.getImportRegistry());
}
this.reader.loadBeanDefinitions(configClasses);
alreadyParsed.addAll(configClasses);

candidates.clear();
if (registry.getBeanDefinitionCount() > candidateNames.length) {
String[] newCandidateNames = registry.getBeanDefinitionNames();
Set<String> oldCandidateNames = new HashSet<>(Arrays.asList(candidateNames));
Set<String> alreadyParsedClasses = new HashSet<>();
for (ConfigurationClass configurationClass : alreadyParsed) {
alreadyParsedClasses.add(configurationClass.getMetadata().getClassName());
}
for (String candidateName : newCandidateNames) {
if (!oldCandidateNames.contains(candidateName)) {
BeanDefinition bd = registry.getBeanDefinition(candidateName);
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, this.metadataReaderFactory) &&
!alreadyParsedClasses.contains(bd.getBeanClassName())) {
candidates.add(new BeanDefinitionHolder(bd, candidateName));
}
}
}
candidateNames = newCandidateNames;
}
}
while (!candidates.isEmpty());

// Register the ImportRegistry as a bean in order to support ImportAware @Configuration classes
if (sbr != null && !sbr.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) {
sbr.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry());
}

if (this.metadataReaderFactory instanceof CachingMetadataReaderFactory) {
// Clear cache in externally provided MetadataReaderFactory; this is a no-op
// for a shared cache since it'll be cleared by the ApplicationContext.
((CachingMetadataReaderFactory) this.metadataReaderFactory).clearCache();
}
}

重点看看 parser.parse(candidates); 这句话是怎么解析的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public void parse(Set<BeanDefinitionHolder> configCandidates) {
for (BeanDefinitionHolder holder : configCandidates) {
BeanDefinition bd = holder.getBeanDefinition();
try {
if (bd instanceof AnnotatedBeanDefinition) {
// 因为这是一个使用注解的Bean,所以应该关注这里
parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
}
else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) {
parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName());
}
else {
parse(bd.getBeanClassName(), holder.getBeanName());
}
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex);
}
}

this.deferredImportSelectorHandler.process();
}

protected final void parse(AnnotationMetadata metadata, String beanName) throws IOException {
processConfigurationClass(new ConfigurationClass(metadata, beanName));
}

protected void processConfigurationClass(ConfigurationClass configClass) throws IOException {
// 如果当前的类是一个解析类,则跳过解析
if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {
return;
}

// 判断是否已经解析过了
ConfigurationClass existingClass = this.configurationClasses.get(configClass);
if (existingClass != null) {
if (configClass.isImported()) {
if (existingClass.isImported()) {
existingClass.mergeImportedBy(configClass);
}
// Otherwise ignore new imported config class; existing non-imported class overrides it.
return;
}
else {
// Explicit bean definition found, probably replacing an import.
// Let's remove the old one and go with the new one.
this.configurationClasses.remove(configClass);
this.knownSuperclasses.values().removeIf(configClass::equals);
}
}

// 递归解析配置类以及他的父类、接口等等
SourceClass sourceClass = asSourceClass(configClass);
do {
sourceClass = doProcessConfigurationClass(configClass, sourceClass);
}
while (sourceClass != null);

this.configurationClasses.put(configClass, configClass);
}

然后我们要进入 doProcessConfigurationClass 观察解析的过程。 这串代码其实有点长……:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
@Nullable
protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)
throws IOException {

if (configClass.getMetadata().isAnnotated(Component.class.getName())) {
// Recursively process any member (nested) classes first
processMemberClasses(configClass, sourceClass);
}

// 解析需要导入Property配置文件的类
for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), PropertySources.class,
org.springframework.context.annotation.PropertySource.class)) {
if (this.environment instanceof ConfigurableEnvironment) {
processPropertySource(propertySource);
}
else {
logger.info("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +
"]. Reason: Environment must implement ConfigurableEnvironment");
}
}

// 解析 @ComponentScan
Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
if (!componentScans.isEmpty() &&
!this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
for (AnnotationAttributes componentScan : componentScans) {
// The config class is annotated with @ComponentScan -> perform the scan immediately
Set<BeanDefinitionHolder> scannedBeanDefinitions =
this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
// Check the set of scanned definitions for any further config classes and parse recursively if needed
for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
if (bdCand == null) {
bdCand = holder.getBeanDefinition();
}
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
parse(bdCand.getBeanClassName(), holder.getBeanName());
}
}
}
}

// 解析 @Import 的类,这句话是重点
processImports(configClass, sourceClass, getImports(sourceClass), true);

// Process any @ImportResource annotations
AnnotationAttributes importResource =
AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class);
if (importResource != null) {
String[] resources = importResource.getStringArray("locations");
Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader");
for (String resource : resources) {
String resolvedResource = this.environment.resolveRequiredPlaceholders(resource);
configClass.addImportedResource(resolvedResource, readerClass);
}
}

// 解析 @Bean 方法
Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);
for (MethodMetadata methodMetadata : beanMethods) {
configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
}

// 解析接口默认方法
processInterfaces(configClass, sourceClass);

// 如果有父类,返回父类,由上一层继续调用这个方法继续解析
if (sourceClass.getMetadata().hasSuperClass()) {
String superclass = sourceClass.getMetadata().getSuperClassName();
if (superclass != null && !superclass.startsWith("java") &&
!this.knownSuperclasses.containsKey(superclass)) {
this.knownSuperclasses.put(superclass, configClass);
// Superclass found, return its annotation metadata and recurse
return sourceClass.getSuperClass();
}
}

// 返回Null表示解析结束
return null;
}

好了,可以看到通过 Java 类的解析可谓覆盖到尼玛我想象不到的地方….但他们就是应该要有 挑重点来看吧,我的主类没有父级也没有接口,直接重点看 processImports(configClass, sourceClass, getImports(sourceClass), true) 这句话。 这句话首先需要获取所有的导入配置 getImports(sourceClass)

1
2
3
4
5
6
private Set<SourceClass> getImports(SourceClass sourceClass) throws IOException {
Set<SourceClass> imports = new LinkedHashSet<>();
Set<SourceClass> visited = new LinkedHashSet<>();
collectImports(sourceClass, imports, visited);
return imports;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
private void collectImports(SourceClass sourceClass, Set<SourceClass> imports, Set<SourceClass> visited)
throws IOException {

if (visited.add(sourceClass)) {
for (SourceClass annotation : sourceClass.getAnnotations()) {
String annName = annotation.getMetadata().getClassName();
if (!annName.equals(Import.class.getName())) {
collectImports(annotation, imports, visited);
}
}
imports.addAll(sourceClass.getAnnotationAttributes(Import.class.getName(), "value"));
}
}

这两个东西,切忌不要去 debug 一个一个看….我在这里看晕了好几个小时,应该是条件断点判断 sourceClass.getAnnotationAttributes(Import.class.getName(), "value").size() > 0 再停住

好了等到断点停住的时候,发现是 @interface SpringBootApplication > @interface EnableAutoConfiguration 上面的 @Import(AutoConfigurationImportSelector.class) 在这里导入了。以及这个类上边的 @AutoConfigurationPackage 导入了 @Import(AutoConfigurationPackages.Registrar.class) 所以当前需要解析的 importCandidatesAutoConfigurationPackages.Registrar.class 以及 AutoConfigurationImportSelector.class 两个类。 那么这两个类是什么用的,我现在也还不知道…先知道他被导入就好了,接下去看 processImports 方法了,这个方法应该有答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
Collection<SourceClass> importCandidates, boolean checkForCircularImports) {

if (importCandidates.isEmpty()) {
return;
}

// 由上面传递的 checkForCircularImports 参数决定是否判断有没有循环引入
if (checkForCircularImports && isChainedImportOnStack(configClass)) {
this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
}
else {
this.importStack.push(configClass);
try {
// 开始解析导入类
for (SourceClass candidate : importCandidates) {
// 第一层,根据不同的导入类型进行解析
// AutoConfigurationImportSelector 来到这个分支
if (candidate.isAssignable(ImportSelector.class)) {
// Candidate class is an ImportSelector -> delegate to it to determine imports
Class<?> candidateClass = candidate.loadClass();
ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class,
this.environment, this.resourceLoader, this.registry);
if (selector instanceof DeferredImportSelector) {
this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
}
else {
String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames);
// 递归解析
processImports(configClass, currentSourceClass, importSourceClasses, false);
}
}
// AutoConfigurationPackages.Registrar进入这个分支
else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
// Candidate class is an ImportBeanDefinitionRegistrar ->
// delegate to it to register additional bean definitions
Class<?> candidateClass = candidate.loadClass();
ImportBeanDefinitionRegistrar registrar =
ParserStrategyUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class,
this.environment, this.resourceLoader, this.registry);
configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
}
else {
// 直接以@Configuration的方式进行处理
this.importStack.registerImport(
currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
processConfigurationClass(candidate.asConfigClass(configClass));
}
}
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to process import candidates for configuration class [" +
configClass.getMetadata().getClassName() + "]", ex);
}
finally {
this.importStack.pop();
}
}
}

不行呀,因为需要根据类型来做,先给两个类的类型吧:

这三个分支对应着三个不同的配置类型:

  1. ImportSelector 用于导入配置类的信息
  2. Registrar 用于解析自定义注解动态生成 Bean 的信息
  3. @Configuration 读取到配置类,直接解析

那我们就先按照上图的顺序来解析这些配置类吧,首先是 AutoConfigurationPackages.Registrar 对象的解析。我先从上面一大串截取代码片段来看:

1
2
3
4
5
6
7
8
9
10
11
// ...
else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
// 额外注册BeanDefinition用
Class<?> candidateClass = candidate.loadClass();
ImportBeanDefinitionRegistrar registrar =
ParserStrategyUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class,
this.environment, this.resourceLoader, this.registry);
// 简单的添加(导入)到当前的 configClass 中
configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
}
// ...

先放到后面,应该是解析的时候会调用到。 接下来看第二个 AutoConfigurationImportSelector.class 的解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (candidate.isAssignable(ImportSelector.class)) {
// Candidate class is an ImportSelector -> delegate to it to determine imports
Class<?> candidateClass = candidate.loadClass();
ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class,
this.environment, this.resourceLoader, this.registry);
if (selector instanceof DeferredImportSelector) {
// 进入这里
this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
}
else {
String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames);
processImports(configClass, currentSourceClass, importSourceClasses, false);
}
}

这一块比较麻烦,在上面 uml 可以看到,AutoConfigurationImportSelector.class 属于 DeferredImportSelector,所以交给 deferredImportSelectorHandler 进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
public void handle(ConfigurationClass configClass, DeferredImportSelector importSelector) {
DeferredImportSelectorHolder holder = new DeferredImportSelectorHolder(
configClass, importSelector);
if (this.deferredImportSelectors == null) {
DeferredImportSelectorGroupingHandler handler = new DeferredImportSelectorGroupingHandler();
handler.register(holder);
handler.processGroupImports();
}
else {
// 来到这里,简单的加入
this.deferredImportSelectors.add(holder);
}
}

导入刚刚注册的配置

上面一大堆全是解析,然后接下来就要处理这些自动配置了。 回到 parser.parse(candidates); 这个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public void parse(Set<BeanDefinitionHolder> configCandidates) {
for (BeanDefinitionHolder holder : configCandidates) {
BeanDefinition bd = holder.getBeanDefinition();
try {
if (bd instanceof AnnotatedBeanDefinition) {
parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
}
else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) {
parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName());
}
else {
parse(bd.getBeanClassName(), holder.getBeanName());
}
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex);
}
}

// 处理导入配置类(第三方配置)
this.deferredImportSelectorHandler.process();
}

AutoConfigurationImportSelector解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void process() {
// AutoConfigurationImportSelector集合 只有一个元素
List<DeferredImportSelectorHolder> deferredImports = this.deferredImportSelectors;
this.deferredImportSelectors = null;
try {
if (deferredImports != null) {
DeferredImportSelectorGroupingHandler handler = new DeferredImportSelectorGroupingHandler();
deferredImports.sort(DEFERRED_IMPORT_COMPARATOR);
// 注册到 DeferredImportSelectorGroupingHandler 中
deferredImports.forEach(handler::register);
// 重点看这边
handler.processGroupImports();
}
}
finally {
this.deferredImportSelectors = new ArrayList<>();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void processGroupImports() {
for (DeferredImportSelectorGrouping grouping : this.groupings.values()) {
grouping.getImports().forEach(entry -> {
ConfigurationClass configurationClass = this.configurationClasses.get(
entry.getMetadata());
try {
processImports(configurationClass, asSourceClass(configurationClass),
asSourceClasses(entry.getImportClassName()), false);
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to process import candidates for configuration class [" +
configurationClass.getMetadata().getClassName() + "]", ex);
}
});
}
}

先进入这里 grouping.getImports()

1
2
3
4
5
6
7
public Iterable<Group.Entry> getImports() {
for (DeferredImportSelectorHolder deferredImport : this.deferredImports) {
this.group.process(deferredImport.getConfigurationClass().getMetadata(),
deferredImport.getImportSelector());
}
return this.group.selectImports();
}

这个过程就是 SpringBoot 提供的,在类路径提供 additional-spring-configuration-metadata.json 提供导入配置的,然而看起来好像所有的 SpringBoot 模块都在这里了,他只是通过导入类路径的包来判断是否要加载配置。 跳过这一段吧,看着有点累。我们现在只要知道,读取了类路径下的 additional-spring-configuration-metadata.json 后,我们想要看到的 MybatisAutoConfiguration 已经被读取到了就好了。

同样的,spring-boot-mybatis-starter 也提供了上面的注册文件:

好了,接下来回到上面的解析方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void processGroupImports() {
for (DeferredImportSelectorGrouping grouping : this.groupings.values()) {
grouping.getImports().forEach(entry -> {
ConfigurationClass configurationClass = this.configurationClasses.get(
entry.getMetadata());
try {
// 此时进入processImports的时候就不需要再导入配置了
// 直接以 @Configuration 去实现配置
processImports(configurationClass, asSourceClass(configurationClass),
asSourceClasses(entry.getImportClassName()), false);
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to process import candidates for configuration class [" +
configurationClass.getMetadata().getClassName() + "]", ex);
}
});
}
}

那么关于 @Configuration 的解析,在我 之前的文章 就可以看到了。 现在就看看怎么根据主类的所在的包,扫描该包下的所有 Bean

自动包扫描注册Bean

我们得回到 doProcessConfigurationClass 的这个地方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 解析 @ComponentScan,会从主类 WebTestApplication 中拿到 BasePackage
// 然后根据这个 BasePackage 的配置进行扫描下面的类
Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
if (!componentScans.isEmpty() &&
!this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
for (AnnotationAttributes componentScan : componentScans) {
// 通过 componentScanParser 进行解析
Set<BeanDefinitionHolder> scannedBeanDefinitions =
this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
// Check the set of scanned definitions for any further config classes and parse recursively if needed
for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
if (bdCand == null) {
bdCand = holder.getBeanDefinition();
}
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
parse(bdCand.getBeanClassName(), holder.getBeanName());
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, final String declaringClass) {
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(this.registry,
componentScan.getBoolean("useDefaultFilters"), this.environment, this.resourceLoader);

Class<? extends BeanNameGenerator> generatorClass = componentScan.getClass("nameGenerator");
boolean useInheritedGenerator = (BeanNameGenerator.class == generatorClass);
scanner.setBeanNameGenerator(useInheritedGenerator ? this.beanNameGenerator :
BeanUtils.instantiateClass(generatorClass));

ScopedProxyMode scopedProxyMode = componentScan.getEnum("scopedProxy");
if (scopedProxyMode != ScopedProxyMode.DEFAULT) {
scanner.setScopedProxyMode(scopedProxyMode);
}
else {
Class<? extends ScopeMetadataResolver> resolverClass = componentScan.getClass("scopeResolver");
scanner.setScopeMetadataResolver(BeanUtils.instantiateClass(resolverClass));
}

scanner.setResourcePattern(componentScan.getString("resourcePattern"));

for (AnnotationAttributes filter : componentScan.getAnnotationArray("includeFilters")) {
for (TypeFilter typeFilter : typeFiltersFor(filter)) {
scanner.addIncludeFilter(typeFilter);
}
}
for (AnnotationAttributes filter : componentScan.getAnnotationArray("excludeFilters")) {
for (TypeFilter typeFilter : typeFiltersFor(filter)) {
scanner.addExcludeFilter(typeFilter);
}
}

boolean lazyInit = componentScan.getBoolean("lazyInit");
if (lazyInit) {
scanner.getBeanDefinitionDefaults().setLazyInit(true);
}

Set<String> basePackages = new LinkedHashSet<>();
String[] basePackagesArray = componentScan.getStringArray("basePackages");
for (String pkg : basePackagesArray) {
String[] tokenized = StringUtils.tokenizeToStringArray(this.environment.resolvePlaceholders(pkg),
ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
Collections.addAll(basePackages, tokenized);
}
for (Class<?> clazz : componentScan.getClassArray("basePackageClasses")) {
basePackages.add(ClassUtils.getPackageName(clazz));
}
if (basePackages.isEmpty()) {
// 没有指定扫描包,进入这里获取主类的包名
basePackages.add(ClassUtils.getPackageName(declaringClass));
}

scanner.addExcludeFilter(new AbstractTypeHierarchyTraversingFilter(false, false) {
@Override
protected boolean matchClassName(String className) {
return declaringClass.equals(className);
}
});

// 上面基本上都是解析配置的一些东西,过滤器啊,LazyInit等等,到这一步才是真正的扫描
// StringUtils.toStringArray(basePackages) 获取包名
return scanner.doScan(StringUtils.toStringArray(basePackages));
}

真正的扫描,做的事情可谓多得多

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
Assert.notEmpty(basePackages, "At least one base package must be specified");
Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
for (String basePackage : basePackages) {
// 这里又是一个大的模块了,也就是读取ClassPath下所有的文件
Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
// 拿到扫描到的 BeanDefinition 注册到工厂中
for (BeanDefinition candidate : candidates) {
ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
candidate.setScope(scopeMetadata.getScopeName());
String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
if (candidate instanceof AbstractBeanDefinition) {
postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
}
if (candidate instanceof AnnotatedBeanDefinition) {
AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
}
if (checkCandidate(beanName, candidate)) {
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
definitionHolder =
AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
beanDefinitions.add(definitionHolder);
registerBeanDefinition(definitionHolder, this.registry);
}
}
}
return beanDefinitions;
}

扫描:

1
2
3
4
5
6
7
8
public Set<BeanDefinition> findCandidateComponents(String basePackage) {
if (this.componentsIndex != null && indexSupportsIncludeFilters()) {
return addCandidateComponentsFromIndex(this.componentsIndex, basePackage);
}
else {
return scanCandidateComponents(basePackage);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
Set<BeanDefinition> candidates = new LinkedHashSet<>();
try {
String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
resolveBasePackage(basePackage) + '/' + this.resourcePattern;
Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
boolean traceEnabled = logger.isTraceEnabled();
boolean debugEnabled = logger.isDebugEnabled();
// 拿到包下两个文件的 Resource 实例
for (Resource resource : resources) {
if (traceEnabled) {
logger.trace("Scanning " + resource);
}
if (resource.isReadable()) {
try {
MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
if (isCandidateComponent(metadataReader)) {
ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
sbd.setResource(resource);
sbd.setSource(resource);
// 符合某种条件,暂且就当BF中还没存在这个Bean吧
if (isCandidateComponent(sbd)) {
if (debugEnabled) {
logger.debug("Identified candidate component class: " + resource);
}
// 添加
candidates.add(sbd);
}
else {
if (debugEnabled) {
logger.debug("Ignored because not a concrete top-level class: " + resource);
}
}
}
else {
if (traceEnabled) {
logger.trace("Ignored because not matching any filter: " + resource);
}
}
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to read candidate component class: " + resource, ex);
}
}
else {
if (traceEnabled) {
logger.trace("Ignored because not readable: " + resource);
}
}
}
}
catch (IOException ex) {
throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex);
}
// 返回
return candidates;
}

OK,扫描完成,后在 finishBeanFactoryInitialization 进行初始化就可以了

结束

这三个分支对应着三个不同的配置类型:

  1. ImportSelector 用于导入配置类的信息
  2. Registrar 用于解析自定义注解动态生成 Bean 的信息
  3. @Configuration 读取到配置类,直接解析

那我们就先按照上图的顺序来解析这些配置类吧,首先是 AutoConfigurationPackages.Registrar 对象的解析。我先从上面一大串截取代码片段来看:

1
2
3
4
5
6
7
8
9
10
11
// ...
else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
// 额外注册BeanDefinition用
Class<?> candidateClass = candidate.loadClass();
ImportBeanDefinitionRegistrar registrar =
ParserStrategyUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class,
this.environment, this.resourceLoader, this.registry);
// 简单的添加(导入)到当前的 configClass 中
configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
}
// ...

先放到后面,应该是解析的时候会调用到。 接下来看第二个 AutoConfigurationImportSelector.class 的解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (candidate.isAssignable(ImportSelector.class)) {
// Candidate class is an ImportSelector -> delegate to it to determine imports
Class<?> candidateClass = candidate.loadClass();
ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class,
this.environment, this.resourceLoader, this.registry);
if (selector instanceof DeferredImportSelector) {
// 进入这里
this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
}
else {
String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames);
processImports(configClass, currentSourceClass, importSourceClasses, false);
}
}

这一块比较麻烦,在上面 uml 可以看到,AutoConfigurationImportSelector.class 属于 DeferredImportSelector,所以交给 deferredImportSelectorHandler 进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
public void handle(ConfigurationClass configClass, DeferredImportSelector importSelector) {
DeferredImportSelectorHolder holder = new DeferredImportSelectorHolder(
configClass, importSelector);
if (this.deferredImportSelectors == null) {
DeferredImportSelectorGroupingHandler handler = new DeferredImportSelectorGroupingHandler();
handler.register(holder);
handler.processGroupImports();
}
else {
// 来到这里,简单的加入
this.deferredImportSelectors.add(holder);
}
}

导入刚刚注册的配置

上面一大堆全是解析,然后接下来就要处理这些自动配置了。 回到 parser.parse(candidates); 这个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public void parse(Set<BeanDefinitionHolder> configCandidates) {
for (BeanDefinitionHolder holder : configCandidates) {
BeanDefinition bd = holder.getBeanDefinition();
try {
if (bd instanceof AnnotatedBeanDefinition) {
parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
}
else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) {
parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName());
}
else {
parse(bd.getBeanClassName(), holder.getBeanName());
}
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex);
}
}

// 处理导入配置类(第三方配置)
this.deferredImportSelectorHandler.process();
}

AutoConfigurationImportSelector解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void process() {
// AutoConfigurationImportSelector集合 只有一个元素
List<DeferredImportSelectorHolder> deferredImports = this.deferredImportSelectors;
this.deferredImportSelectors = null;
try {
if (deferredImports != null) {
DeferredImportSelectorGroupingHandler handler = new DeferredImportSelectorGroupingHandler();
deferredImports.sort(DEFERRED_IMPORT_COMPARATOR);
// 注册到 DeferredImportSelectorGroupingHandler 中
deferredImports.forEach(handler::register);
// 重点看这边
handler.processGroupImports();
}
}
finally {
this.deferredImportSelectors = new ArrayList<>();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void processGroupImports() {
for (DeferredImportSelectorGrouping grouping : this.groupings.values()) {
grouping.getImports().forEach(entry -> {
ConfigurationClass configurationClass = this.configurationClasses.get(
entry.getMetadata());
try {
processImports(configurationClass, asSourceClass(configurationClass),
asSourceClasses(entry.getImportClassName()), false);
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to process import candidates for configuration class [" +
configurationClass.getMetadata().getClassName() + "]", ex);
}
});
}
}

先进入这里 grouping.getImports()

1
2
3
4
5
6
7
public Iterable<Group.Entry> getImports() {
for (DeferredImportSelectorHolder deferredImport : this.deferredImports) {
this.group.process(deferredImport.getConfigurationClass().getMetadata(),
deferredImport.getImportSelector());
}
return this.group.selectImports();
}

这个过程就是 SpringBoot 提供的,在类路径提供 additional-spring-configuration-metadata.json 提供导入配置的,然而看起来好像所有的 SpringBoot 模块都在这里了,他只是通过导入类路径的包来判断是否要加载配置。 跳过这一段吧,看着有点累。我们现在只要知道,读取了类路径下的 additional-spring-configuration-metadata.json 后,我们想要看到的 MybatisAutoConfiguration 已经被读取到了就好了。

SpringBoot

OK,现代 Java 开发者应该都对 SpringBoot 很熟悉了吧,一个很 "轻量级"IOC 容器。 我记得 SpringBoot 刚出来的时候,很多博客文章都会说 SpringBoot 减轻了 Java 开发工作者的负担,是个轻量级的框架。然而后面我才发现,并不轻量级。因为,SpringBoot 把需要依赖的东西给封装了起来,但其实比起之前自己控制依赖项目来说,反而会更重了一些,毕竟以前还可以自由组合。现在,SpringBoot 以及框架作者都提供了默认的依赖以及默认的配置,所以说这个框架轻量级其实并不是,要说轻量级应该只是说开发轻量级而已,菜鸟也可以快速上手建立一个后台项目而不必去关心太多项目配置的东西。 框架肯定是一个优秀的框架,我们项目全体也都是 SpringBoot 架构起来的,所以还是需要看看,SpringBoot 偷偷帮我们做了什么事情。

依赖简化

日常使用中,我们只需要引入一个 starter,就可以神奇把我们整合的框架整合起来。但是在以前,我们要使用 MyBatis 的时候,却需要引入 org.mybatis:mybatis 主框架,然后还因为需要整合 Spring 框架,所以我们还需要引入一个 org.mybatis:mybatis-spring。这就算了,这两个包如果版本号对不上,还要出现兼容性的问题。 然而现在我们只需要一个 org.mybatis.spring.boot:mybatis-spring-boot-starter,就可以同时引入这两个包,而且版本号还解决的很OK。所以现在我们可以看看他的 pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot</artifactId>
<version>1.3.1</version>
</parent>
<artifactId>mybatis-spring-boot-starter</artifactId>
<name>mybatis-spring-boot-starter</name>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
</dependency>
</dependencies>
</project>

所以结论是,一个 starter,定义了需要依赖的包的版本,然后通过依赖传递将这些包传递到我们 starter 所在的项目上来。 目前我建立了一个项目,这个项目很简单,只是依赖了一个 spring-boot-starter-web, 我们可以在 idea 上很方便的查看依赖的所有东西:

但其实这个做法在以前 Spring 的时候是已经存在了,这个项目就是 Spring-IO,但我也不知道为什么,可能因为名字取得不够好吧?然后这个项目好像很少人用。或者以前的人觉得会被依赖很多东西进来所以不用这个项目了?

自动装配

我们项目所需要的依赖都弄进来了,下一步就是配置让这些包可以相互配合,共同提供服务呀。 所以,SpringBoot 所提供的第二个功能就是,根据默认的配置,装配依赖进来包里面的 Bean 实例。依赖+装配,就是我们以前一直所需要的操作了,可以说这个框架减少了全世界程序员按 CTRL+C/V 的次数hhhhh。 OK,简单了解的话,我需要一个额外的 starter,这里就使用 org.mybatis.spring.boot:mybatis-spring-boot-starter 来看,仓库地址 项目的目录结构为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
├── LICENSE
├── README.md
├── license.txt
├── mvnw
├── mvnw.cmd
├── mybatis-spring-boot-autoconfigure
│   ├── format.xml
│   ├── license.txt
│   ├── pom.xml
│   └── src
├── mybatis-spring-boot-samples
│   ├── mybatis-spring-boot-sample-annotation
│   ├── mybatis-spring-boot-sample-freemarker
│   ├── mybatis-spring-boot-sample-freemarker-legacy
│   ├── mybatis-spring-boot-sample-groovy
│   ├── mybatis-spring-boot-sample-kotlin
│   ├── mybatis-spring-boot-sample-thymeleaf
│   ├── mybatis-spring-boot-sample-velocity
│   ├── mybatis-spring-boot-sample-velocity-legacy
│   ├── mybatis-spring-boot-sample-war
│   ├── mybatis-spring-boot-sample-web
│   ├── mybatis-spring-boot-sample-xml
│   └── pom.xml
├── mybatis-spring-boot-starter
│   ├── license.txt
│   └── pom.xml
├── mybatis-spring-boot-starter-test
│   └── pom.xml
├── mybatis-spring-boot-test-autoconfigure
│   ├── format.xml
│   ├── pom.xml
│   └── src
├── pom.xml
└── travis
├── after_success.sh
└── settings.xml

其中,mybatis-spring-boot-autoconfigure 比较惹人注目,所以我们现在就看看这个项目。 首先我们看看 main/resources/META-INF/spring.factories,因为 Spring 很喜欢通过这些 meta 文件来促使各个模块很好的解耦但又能彼此配合工作,所以这个文件是定义 自动装配 开始的工厂类:

1
2
3
4
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration,\
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration

那么通过看初步的源码,这两个类都是熟悉的 Java 装配类,那么将会被加入到前面提到的 BeanFactory 容器中,后面解析将会调用里面的配置方法。 第二个优秀的地方就是,定义配置类,就可以在 yaml 文件中提示的出现,可以利用 IDE 工具很好的防止配置名写错。这个类就是 MybatisProperties.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@ConfigurationProperties(prefix = MybatisProperties.MYBATIS_PREFIX)
public class MybatisProperties {

public static final String MYBATIS_PREFIX = "mybatis";

private static final ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();

/**
* Location of MyBatis xml config file.
*/
private String configLocation;

/**
* Locations of MyBatis mapper files.
*/
private String[] mapperLocations;

...
}

OK,我们可以看到,定义了前缀就是 mybatis,而这个类是个贫血型的 Bean,只有属性。这些属性将会贯穿 MyBatis 在项目中整个生命周期。 接下来一个问题,我并不需要每个属性都在 yaml 文件中去定义啊,有些直接使用官方提供的默认值就可以了。 所以官方又贴心的提供了 additional-spring-configuration-metadata.json 这个 JSON 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
{
"properties": [
{
"sourceType": "org.apache.ibatis.session.Configuration",
"defaultValue": "org.apache.ibatis.scripting.xmltags.XMLLanguageDriver",
"name": "mybatis.configuration.default-scripting-language",
"description": "A default LanguageDriver class.",
"type": "java.lang.Class<? extends org.apache.ibatis.scripting.LanguageDriver>",
"deprecation": {
"reason": "Because when this configuration property is used, there is case that custom language driver cannot be registered correctly.",
"replacement": "mybatis.default-scripting-language-driver"
}
},
{
"sourceType": "org.apache.ibatis.session.Configuration",
"defaultValue": "org.apache.ibatis.type.EnumTypeHandler",
"name": "mybatis.configuration.default-enum-type-handler",
"description": "A default TypeHandler class for Enum.",
"type": "java.lang.Class<? extends org.apache.ibatis.type.TypeHandler>"
},
{
"defaultValue": false,
"name": "mybatis.lazy-initialization",
"description": "Set whether enable lazy initialization for mapper bean.",
"type": "java.lang.Boolean"
},
{
"name": "mybatis.scripting-language-driver.velocity.userdirective",
"deprecation": {
"level": "error",
"reason": "The 'userdirective' is deprecated since Velocity 2.x. This property defined for keeping backward compatibility with older velocity version.",
"replacement": "mybatis.scripting-language-driver.velocity.velocity-settings.runtime.custom_directives"
}
}

]
}

不好理解的话,我们直接看第三个默认配置就好了:

1
2
3
4
5
6
{
"defaultValue": false,
"name": "mybatis.lazy-initialization",
"description": "Set whether enable lazy initialization for mapper bean.",
"type": "java.lang.Boolean"
}

看了下图应该明白了吧,上面那个文件就是定义配置的一些说明、默认值的。

所以当我们要造一个框架,又因为很多约定的东西,靠人脑来记已经靠不住的情况下,就可以编写类似于 additional-spring-configuration-metadata.json 这种文件来做约定以及说明了。


OK,上面说完了 SpringBoot 的两个最主要的优点以后,现在就来看看源码。但是源码这块,因为 传递依赖 利用的是 mvn/gradle 的特性,所以 依赖传递 并不需要再说。 那么最主要的就是来解析,怎么自动装配的问题。

简单的SpringBoot项目

pom.xml 依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.BUILD-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.liweidan.web</groupId>
<artifactId>web-test</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>web-test</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

一个主启动器:

1
2
3
4
5
6
7
8
@SpringBootApplication
public class WebTestApplication {

public static void main(String[] args) {
SpringApplication.run(WebTestApplication.class, args);
}

}

一个控制器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
public class UserController {

@GetMapping("users")
public List<Map<String, String>> users() {
List<Map<String, String>> users = new ArrayList<>();
for (int i = 0; i < 3; i++) {
Map<String, String> u = new HashMap<>();
u.put("id", String.valueOf(i));
u.put("name", "name" + i);
users.add(u);
}
return users;
}

}

SpringApplication元信息准备

写过 SpringBoot 项目的同学应该对这个很熟悉了,通过传递一个上下文的根类,SpringBoot 将会自动装载在此类所在的包下面的所有类,并且 args 很明显就是我们在控制台传递的参数,也一并传递给 SpringApplication.run 这个方法,SpringBoot 即可将命令行的参数配置覆盖配置文件指定的配置。 接下来看看这个类做了什么事情:

1
2
3
4
5
6
7
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
return run(new Class<?>[] { primarySource }, args);
}

public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
return new SpringApplication(primarySources).run(args);
}

通过静态方法,传递一个 Class 类作为主资源,然后再传递给主资源数组的 run 方法。那么这个数组其实我们是可以传递多个主资源的,比如我们做项目的时候,想要每个模块包彼此分离,即可传递多个包的主资源路径。 new SpringApplication(primarySources).run(args); 才是真正的进入容器的准备阶段: 首先看看构造器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
// 初始化主资源链表,用于下面需要读取的时候可以遍历.
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
// 判断Web环境,有webFlux/web/普通三个环境,主要通过类路径是否带有相对应需要的类来判断,如果都没有则初始化为普通Java项目.
this.webApplicationType = WebApplicationType.deduceFromClasspath();
// 初始化环境,这个接口多用于web环境,因为需要从web上下文加载一些信息.
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
// 初始化监听器,监听容器生命周期中需要回调的函数
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
// 从运行堆栈中寻找运行这个类的main方法所在的类
this.mainApplicationClass = deduceMainApplicationClass();
}

加载不同模块的元信息

在上面的构造器中看到 setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));Spring 还是跟以前获取 handlers 一样。通过在 META-INF 类加载路径定义不同的 spring.factories,使用类加载器读取这些配置文件资源,解析,加载配置文件中的类,初始化对象,来共同完成业务。

1
2
3
4
5
6
7
8
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
ClassLoader classLoader = getClassLoader();
// Use names and ensure unique to protect against duplicates
Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
AnnotationAwareOrderComparator.sort(instances);
return instances;
}

通过 SpringFactoriesLoader.loadFactoryNames(type, classLoader),加载当前 ClassLoader 中的指定类,源码显示如何读取,后面的 getOrDefault(factoryTypeName, Collections.emptyList()) 还是比较好理解的,如果加载到工厂类的名字就返回不然返回个空集合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
String factoryTypeName = factoryType.getName();
return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
}

private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
// 相当于Map<String, List<String>>结构,先从缓存命中.
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null) {
return result;
}

/*
属性定义了元信息的路径以及名字
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
*/
try {
// 开始加载类路径下特定名字的元信息文件
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
result = new LinkedMultiValueMap<>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
// 通过UrlResource解析成Properties对象
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
// 遍历所有模块的指定文件,然后加入缓存
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryTypeName = ((String) entry.getKey()).trim();
for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
result.add(factoryTypeName, factoryImplementationName.trim());
}
}
}
cache.put(classLoader, result);
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}

启动项目SpringApplication.run

可以看到 run 的源码跟之前的 AbstractApplicationContext#refresh 的味道还是一样的,先加载一系列容器运行时需要的生命周期类(Spring 模块间的也有用户自定义的),然后 refreshContext(context) 刷新容器上下文。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public ConfigurableApplicationContext run(String... args) {
// 这是用来打印加载时间的工具类,暂时可以略过.
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
// 主要设置JVM虚拟机支持无设备情况下让awt可运行的属性
configureHeadlessProperty();
// 一. 加载所有SpringApplicationRunListeners并开始遍历所有Lintener启动监听回调函数
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// 二. 开始准备ConfigurableEnvironment环境
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
// 三. 创建应用上下文
context = createApplicationContext();
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
// 四. 做一些准备工作
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
// 五. 刷新上下文
refreshContext(context);
// 六. 刷新完成后做的一些清理、回调工作
afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
// 七. 启动完成后,调用所有SpringApplicationRunListener的完成启动的回调函数
listeners.started(context);
// 八. 主要处理ApplicationRunner和CommandLineRunner的回调
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}

try {
// 九. 运行时的SpringApplicationRunListener回调函数
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
return context;
}

装载SpringApplicationRunListener

1
2
3
4
5
private SpringApplicationRunListeners getRunListeners(String[] args) {
Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
return new SpringApplicationRunListeners(logger,
getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args));
}

首先我们看第一步,加载所有的 SpringApplicationRunListener 子类,这个加载方式跟上面所说的加载元信息是一样的,只不过指定加载 SpringApplicationRunListener.class 类。 我们知道,Spring 经常会定义很多生命周期回调,供用户根据需求切入框架。这次 SpringBoot 的生命周期是 SpringApplicationRunListener。 首先看看 SpringApplicationRunListener 定义哪些生命周期函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public interface SpringApplicationRunListener {

// 项目开始时调用
default void starting() {
}

// 环境准备好时
default void environmentPrepared(ConfigurableEnvironment environment) {
}

// 上下文准备好时
default void contextPrepared(ConfigurableApplicationContext context) {
}

// 上下文读取完成
default void contextLoaded(ConfigurableApplicationContext context) {
}

// 启动完成
default void started(ConfigurableApplicationContext context) {
}

// 项目运行时调用
default void running(ConfigurableApplicationContext context) {
}

// 项目失败时
default void failed(ConfigurableApplicationContext context, Throwable exception) {
}

}

准备ConfigurableEnvironment环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments) {
// 首先根据环境类型获取对象的配置实现对象
ConfigurableEnvironment environment = getOrCreateEnvironment();
// 配置ConversionService单例对象,以及判断有没有设置Profiles环境
configureEnvironment(environment, applicationArguments.getSourceArgs());
// 读取其他环境中(比如web.xml)读取的配置属性
ConfigurationPropertySources.attach(environment);
// 继续上面的SpringApplicationRunListener生命周期调用
listeners.environmentPrepared(environment);
// 把环境绑定到Binder中,Binder是Spring提供的一个记录对象的容器
bindToSpringApplication(environment);
if (!this.isCustomEnvironment) {
// 如果当前的配置环境和重新判断的环境不同,则转换成当前的环境(有可能在生命周期修改了加载的环境?)
environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
deduceEnvironmentClass());
}
// 重新读取特定环境的配置
ConfigurationPropertySources.attach(environment);
return environment;
}

private ConfigurableEnvironment getOrCreateEnvironment() {
if (this.environment != null) {
return this.environment;
}
switch (this.webApplicationType) {
case SERVLET:
return new StandardServletEnvironment();
case REACTIVE:
return new StandardReactiveWebEnvironment();
default:
return new StandardEnvironment();
}
}

然后在主线程中配置忽略 BooleanBean

1
2
3
4
5
6
7
8
9
10
11
12
13
public ConfigurableApplicationContext run(String... args) {
//...
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);
//...
}

private void configureIgnoreBeanInfo(ConfigurableEnvironment environment) {
if (System.getProperty(CachedIntrospectionResults.IGNORE_BEANINFO_PROPERTY_NAME) == null) {
Boolean ignore = environment.getProperty("spring.beaninfo.ignore", Boolean.class, Boolean.TRUE);
System.setProperty(CachedIntrospectionResults.IGNORE_BEANINFO_PROPERTY_NAME, ignore.toString());
}
}

创建上下文

1
2
3
4
context = createApplicationContext();
// 获取模块的错误解释器,用于项目启动的时候解析是什么错误
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);

根据不同环境创建不同的上下文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
protected ConfigurableApplicationContext createApplicationContext() {
Class<?> contextClass = this.applicationContextClass;
if (contextClass == null) {
try {
switch (this.webApplicationType) {
case SERVLET:
contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);
break;
case REACTIVE:
contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
break;
default:
contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
}
}
catch (ClassNotFoundException ex) {
throw new IllegalStateException(
"Unable create a default ApplicationContext, please specify an ApplicationContextClass", ex);
}
}
return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
}

Context准备工作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment,
SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
// 设置上面准备好的环境信息
context.setEnvironment(environment);
// 处理之前做一些事情,但是当前环境下,只是注册了一个 ConversionService 到 BeanFactory 中
postProcessApplicationContext(context);
// 调用之前注册的所有Initializers
applyInitializers(context);
// 再调用所有的listeners
listeners.contextPrepared(context);
if (this.logStartupInfo) {
logStartupInfo(context.getParent() == null);
logStartupProfileInfo(context);
}
// 注册SpringBoot独有的一些Bean
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
if (printedBanner != null) {
beanFactory.registerSingleton("springBootBanner", printedBanner);
}
// 接下来跟之前一样
if (beanFactory instanceof DefaultListableBeanFactory) {
((DefaultListableBeanFactory) beanFactory)
.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
}
if (this.lazyInitialization) {
context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
}
// Load the sources
Set<Object> sources = getAllSources();
Assert.notEmpty(sources, "Sources must not be empty");
// 根据我们传递的主类进行读取,放入BeanFactory中
load(context, sources.toArray(new Object[0]));
listeners.contextLoaded(context);
}

其实当前 postProcessApplicationContext 除了注册 ConversionService 以外其他事情都没做。

refreshContext刷新上下文

因为我要看看自动装配的问题,所以这个时候为了故事比较好说,我加了 mybatis-spring-boot-starter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.BUILD-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.liweidan.web</groupId>
<artifactId>web-test</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>web-test</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.197</version>
</dependency>

</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

刷新上下文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void refreshContext(ConfigurableApplicationContext context) {
refresh(context);
if (this.registerShutdownHook) {
try {
context.registerShutdownHook();
}
catch (AccessControlException ex) {
// Not allowed in some environments.
}
}
}
protected void refresh(ApplicationContext applicationContext) {
Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext);
((AbstractApplicationContext) applicationContext).refresh();
}

然后接下来就去到我们熟悉的 AbstractApplicationContext 中,重温一下这个 refresh() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
prepareRefresh();

// Tell the subclass to refresh the internal bean factory.
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);

try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);

// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);

// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);

// Initialize message source for this context.
initMessageSource();

// Initialize event multicaster for this context.
initApplicationEventMulticaster();

// Initialize other special beans in specific context subclasses.
onRefresh();

// Check for listener beans and register them.
registerListeners();

// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);

// Last step: publish corresponding event.
finishRefresh();
}

catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}

// Destroy already created singletons to avoid dangling resources.
destroyBeans();

// Reset 'active' flag.
cancelRefresh(ex);

// Propagate exception to caller.
throw ex;
}

finally {
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
resetCommonCaches();
}
}
}

同之前一样,在 invokeBeanFactoryPostProcessors(beanFactory) 这句话出现了 BeanDefinition 数量的剧增,那么我们可以推断出,这句话还是关键,就是在这句话开始解析我们的项目依赖。

其实按照我的猜测,自动配置应该是 starter 提供了一些配置类交给 SpringBoot 注入 Spring 的 BeanFactory 中,但是现在看来,好像 SpringBoot 就不需要做什么东西,直接委托给容器扫描解析执行了。

invokeBeanFactoryPostProcessors

1
2
3
4
5
6
7
8
9
10
11
protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
// 利用委派模式委托给 PostProcessorRegistrationDelegate 进行执行
PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());

// Detect a LoadTimeWeaver and prepare for weaving, if found in the meantime
// (e.g. through an @Bean method registered by ConfigurationClassPostProcessor)
if (beanFactory.getTempClassLoader() == null && beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) {
beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));
beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
}
}

PostProcessorRegistrationDelegate委派对象

这是一个在 Context 模块下对象,主要处理 PostProcessor 相关的事情,看到签名只提供两个方法:

  • invokeBeanFactoryPostProcessors 执行 BF 的 PostProcessors
  • registerBeanPostProcessors 注册 PostProcessors 到 BF 中

关于普通的配置类解析,之前在 chapters3_高级的Beafctory—Spring上下文 已经有提到,解析项目中配置类不明白的话可以先看看那篇文章。 不过我们现在重点是怎么读取到 starter 框架里边的自动配置信息:

1
2
3
4
5
6
7
private static void invokeBeanDefinitionRegistryPostProcessors(
Collection<? extends BeanDefinitionRegistryPostProcessor> postProcessors, BeanDefinitionRegistry registry) {

for (BeanDefinitionRegistryPostProcessor postProcessor : postProcessors) {
postProcessor.postProcessBeanDefinitionRegistry(registry);
}
}

当前 postProcessors 里边只有一个 ConfigurationClassPostProcessor 那我们进入对应的 postProcessBeanDefinitionRegistry 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
int registryId = System.identityHashCode(registry);
if (this.registriesPostProcessed.contains(registryId)) {
throw new IllegalStateException(
"postProcessBeanDefinitionRegistry already called on this post-processor against " + registry);
}
if (this.factoriesPostProcessed.contains(registryId)) {
throw new IllegalStateException(
"postProcessBeanFactory already called on this post-processor against " + registry);
}
this.registriesPostProcessed.add(registryId);

processConfigBeanDefinitions(registry);
}

processConfigBeanDefinitions 解析配置

由于我们现在需要跟自动导入的配置,所以我们应该需要跟的类配置是我们的主类,也就是 WebTestApplication 这个类,这个类上修饰的注解 @SpringBootApplication 是自动注入的关键

这个才是执行自动配置的重点之重:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
/**
* Build and validate a configuration model based on the registry of
* {@link Configuration} classes.
*/
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
List<BeanDefinitionHolder> configCandidates = new ArrayList<>();
String[] candidateNames = registry.getBeanDefinitionNames();

for (String beanName : candidateNames) {
BeanDefinition beanDef = registry.getBeanDefinition(beanName);
if (beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE) != null) {
if (logger.isDebugEnabled()) {
logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);
}
}
else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
}
}

// Return immediately if no @Configuration classes were found
if (configCandidates.isEmpty()) {
return;
}

// Sort by previously determined @Order value, if applicable
configCandidates.sort((bd1, bd2) -> {
int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition());
int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition());
return Integer.compare(i1, i2);
});

// Detect any custom bean name generation strategy supplied through the enclosing application context
SingletonBeanRegistry sbr = null;
if (registry instanceof SingletonBeanRegistry) {
sbr = (SingletonBeanRegistry) registry;
if (!this.localBeanNameGeneratorSet) {
BeanNameGenerator generator = (BeanNameGenerator) sbr.getSingleton(
AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR);
if (generator != null) {
this.componentScanBeanNameGenerator = generator;
this.importBeanNameGenerator = generator;
}
}
}

if (this.environment == null) {
this.environment = new StandardEnvironment();
}

// Parse each @Configuration class
ConfigurationClassParser parser = new ConfigurationClassParser(
this.metadataReaderFactory, this.problemReporter, this.environment,
this.resourceLoader, this.componentScanBeanNameGenerator, registry);

Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());
// ↑ 这上面属于解析开始的准备工作 ↑
do {
parser.parse(candidates);
parser.validate();

Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
configClasses.removeAll(alreadyParsed);

// Read the model and create bean definitions based on its content
if (this.reader == null) {
this.reader = new ConfigurationClassBeanDefinitionReader(
registry, this.sourceExtractor, this.resourceLoader, this.environment,
this.importBeanNameGenerator, parser.getImportRegistry());
}
this.reader.loadBeanDefinitions(configClasses);
alreadyParsed.addAll(configClasses);

candidates.clear();
if (registry.getBeanDefinitionCount() > candidateNames.length) {
String[] newCandidateNames = registry.getBeanDefinitionNames();
Set<String> oldCandidateNames = new HashSet<>(Arrays.asList(candidateNames));
Set<String> alreadyParsedClasses = new HashSet<>();
for (ConfigurationClass configurationClass : alreadyParsed) {
alreadyParsedClasses.add(configurationClass.getMetadata().getClassName());
}
for (String candidateName : newCandidateNames) {
if (!oldCandidateNames.contains(candidateName)) {
BeanDefinition bd = registry.getBeanDefinition(candidateName);
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, this.metadataReaderFactory) &&
!alreadyParsedClasses.contains(bd.getBeanClassName())) {
candidates.add(new BeanDefinitionHolder(bd, candidateName));
}
}
}
candidateNames = newCandidateNames;
}
}
while (!candidates.isEmpty());

// Register the ImportRegistry as a bean in order to support ImportAware @Configuration classes
if (sbr != null && !sbr.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) {
sbr.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry());
}

if (this.metadataReaderFactory instanceof CachingMetadataReaderFactory) {
// Clear cache in externally provided MetadataReaderFactory; this is a no-op
// for a shared cache since it'll be cleared by the ApplicationContext.
((CachingMetadataReaderFactory) this.metadataReaderFactory).clearCache();
}
}

重点看看 parser.parse(candidates); 这句话是怎么解析的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public void parse(Set<BeanDefinitionHolder> configCandidates) {
for (BeanDefinitionHolder holder : configCandidates) {
BeanDefinition bd = holder.getBeanDefinition();
try {
if (bd instanceof AnnotatedBeanDefinition) {
// 因为这是一个使用注解的Bean,所以应该关注这里
parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
}
else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) {
parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName());
}
else {
parse(bd.getBeanClassName(), holder.getBeanName());
}
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex);
}
}

this.deferredImportSelectorHandler.process();
}

protected final void parse(AnnotationMetadata metadata, String beanName) throws IOException {
processConfigurationClass(new ConfigurationClass(metadata, beanName));
}

protected void processConfigurationClass(ConfigurationClass configClass) throws IOException {
// 如果当前的类是一个解析类,则跳过解析
if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {
return;
}

// 判断是否已经解析过了
ConfigurationClass existingClass = this.configurationClasses.get(configClass);
if (existingClass != null) {
if (configClass.isImported()) {
if (existingClass.isImported()) {
existingClass.mergeImportedBy(configClass);
}
// Otherwise ignore new imported config class; existing non-imported class overrides it.
return;
}
else {
// Explicit bean definition found, probably replacing an import.
// Let's remove the old one and go with the new one.
this.configurationClasses.remove(configClass);
this.knownSuperclasses.values().removeIf(configClass::equals);
}
}

// 递归解析配置类以及他的父类、接口等等
SourceClass sourceClass = asSourceClass(configClass);
do {
sourceClass = doProcessConfigurationClass(configClass, sourceClass);
}
while (sourceClass != null);

this.configurationClasses.put(configClass, configClass);
}

然后我们要进入 doProcessConfigurationClass 观察解析的过程。 这串代码其实有点长……:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
@Nullable
protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)
throws IOException {

if (configClass.getMetadata().isAnnotated(Component.class.getName())) {
// Recursively process any member (nested) classes first
processMemberClasses(configClass, sourceClass);
}

// 解析需要导入Property配置文件的类
for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), PropertySources.class,
org.springframework.context.annotation.PropertySource.class)) {
if (this.environment instanceof ConfigurableEnvironment) {
processPropertySource(propertySource);
}
else {
logger.info("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +
"]. Reason: Environment must implement ConfigurableEnvironment");
}
}

// 解析 @ComponentScan
Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
if (!componentScans.isEmpty() &&
!this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
for (AnnotationAttributes componentScan : componentScans) {
// The config class is annotated with @ComponentScan -> perform the scan immediately
Set<BeanDefinitionHolder> scannedBeanDefinitions =
this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
// Check the set of scanned definitions for any further config classes and parse recursively if needed
for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
if (bdCand == null) {
bdCand = holder.getBeanDefinition();
}
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
parse(bdCand.getBeanClassName(), holder.getBeanName());
}
}
}
}

// 解析 @Import 的类,这句话是重点
processImports(configClass, sourceClass, getImports(sourceClass), true);

// Process any @ImportResource annotations
AnnotationAttributes importResource =
AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class);
if (importResource != null) {
String[] resources = importResource.getStringArray("locations");
Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader");
for (String resource : resources) {
String resolvedResource = this.environment.resolveRequiredPlaceholders(resource);
configClass.addImportedResource(resolvedResource, readerClass);
}
}

// 解析 @Bean 方法
Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);
for (MethodMetadata methodMetadata : beanMethods) {
configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
}

// 解析接口默认方法
processInterfaces(configClass, sourceClass);

// 如果有父类,返回父类,由上一层继续调用这个方法继续解析
if (sourceClass.getMetadata().hasSuperClass()) {
String superclass = sourceClass.getMetadata().getSuperClassName();
if (superclass != null && !superclass.startsWith("java") &&
!this.knownSuperclasses.containsKey(superclass)) {
this.knownSuperclasses.put(superclass, configClass);
// Superclass found, return its annotation metadata and recurse
return sourceClass.getSuperClass();
}
}

// 返回Null表示解析结束
return null;
}

好了,可以看到通过 Java 类的解析可谓覆盖到尼玛我想象不到的地方….但他们就是应该要有 挑重点来看吧,我的主类没有父级也没有接口,直接重点看 processImports(configClass, sourceClass, getImports(sourceClass), true) 这句话。 这句话首先需要获取所有的导入配置 getImports(sourceClass)

1
2
3
4
5
6
private Set<SourceClass> getImports(SourceClass sourceClass) throws IOException {
Set<SourceClass> imports = new LinkedHashSet<>();
Set<SourceClass> visited = new LinkedHashSet<>();
collectImports(sourceClass, imports, visited);
return imports;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
private void collectImports(SourceClass sourceClass, Set<SourceClass> imports, Set<SourceClass> visited)
throws IOException {

if (visited.add(sourceClass)) {
for (SourceClass annotation : sourceClass.getAnnotations()) {
String annName = annotation.getMetadata().getClassName();
if (!annName.equals(Import.class.getName())) {
collectImports(annotation, imports, visited);
}
}
imports.addAll(sourceClass.getAnnotationAttributes(Import.class.getName(), "value"));
}
}

这两个东西,切忌不要去 debug 一个一个看….我在这里看晕了好几个小时,应该是条件断点判断 sourceClass.getAnnotationAttributes(Import.class.getName(), "value").size() > 0 再停住

好了等到断点停住的时候,发现是 @interface SpringBootApplication > @interface EnableAutoConfiguration 上面的 @Import(AutoConfigurationImportSelector.class) 在这里导入了。以及这个类上边的 @AutoConfigurationPackage 导入了 @Import(AutoConfigurationPackages.Registrar.class) 所以当前需要解析的 importCandidatesAutoConfigurationPackages.Registrar.class 以及 AutoConfigurationImportSelector.class 两个类。 那么这两个类是什么用的,我现在也还不知道…先知道他被导入就好了,接下去看 processImports 方法了,这个方法应该有答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
Collection<SourceClass> importCandidates, boolean checkForCircularImports) {

if (importCandidates.isEmpty()) {
return;
}

// 由上面传递的 checkForCircularImports 参数决定是否判断有没有循环引入
if (checkForCircularImports && isChainedImportOnStack(configClass)) {
this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
}
else {
this.importStack.push(configClass);
try {
// 开始解析导入类
for (SourceClass candidate : importCandidates) {
// 第一层,根据不同的导入类型进行解析
// AutoConfigurationImportSelector 来到这个分支
if (candidate.isAssignable(ImportSelector.class)) {
// Candidate class is an ImportSelector -> delegate to it to determine imports
Class<?> candidateClass = candidate.loadClass();
ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class,
this.environment, this.resourceLoader, this.registry);
if (selector instanceof DeferredImportSelector) {
this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
}
else {
String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames);
// 递归解析
processImports(configClass, currentSourceClass, importSourceClasses, false);
}
}
// AutoConfigurationPackages.Registrar进入这个分支
else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
// Candidate class is an ImportBeanDefinitionRegistrar ->
// delegate to it to register additional bean definitions
Class<?> candidateClass = candidate.loadClass();
ImportBeanDefinitionRegistrar registrar =
ParserStrategyUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class,
this.environment, this.resourceLoader, this.registry);
configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
}
else {
// 直接以@Configuration的方式进行处理
this.importStack.registerImport(
currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
processConfigurationClass(candidate.asConfigClass(configClass));
}
}
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to process import candidates for configuration class [" +
configClass.getMetadata().getClassName() + "]", ex);
}
finally {
this.importStack.pop();
}
}
}

不行呀,因为需要根据类型来做,先给两个类的类型吧:

这三个分支对应着三个不同的配置类型:

  1. ImportSelector 用于导入配置类的信息
  2. Registrar 用于解析自定义注解动态生成 Bean 的信息
  3. @Configuration 读取到配置类,直接解析

那我们就先按照上图的顺序来解析这些配置类吧,首先是 AutoConfigurationPackages.Registrar 对象的解析。我先从上面一大串截取代码片段来看:

1
2
3
4
5
6
7
8
9
10
11
// ...
else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
// 额外注册BeanDefinition用
Class<?> candidateClass = candidate.loadClass();
ImportBeanDefinitionRegistrar registrar =
ParserStrategyUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class,
this.environment, this.resourceLoader, this.registry);
// 简单的添加(导入)到当前的 configClass 中
configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
}
// ...

先放到后面,应该是解析的时候会调用到。 接下来看第二个 AutoConfigurationImportSelector.class 的解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (candidate.isAssignable(ImportSelector.class)) {
// Candidate class is an ImportSelector -> delegate to it to determine imports
Class<?> candidateClass = candidate.loadClass();
ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class,
this.environment, this.resourceLoader, this.registry);
if (selector instanceof DeferredImportSelector) {
// 进入这里
this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
}
else {
String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames);
processImports(configClass, currentSourceClass, importSourceClasses, false);
}
}

这一块比较麻烦,在上面 uml 可以看到,AutoConfigurationImportSelector.class 属于 DeferredImportSelector,所以交给 deferredImportSelectorHandler 进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
public void handle(ConfigurationClass configClass, DeferredImportSelector importSelector) {
DeferredImportSelectorHolder holder = new DeferredImportSelectorHolder(
configClass, importSelector);
if (this.deferredImportSelectors == null) {
DeferredImportSelectorGroupingHandler handler = new DeferredImportSelectorGroupingHandler();
handler.register(holder);
handler.processGroupImports();
}
else {
// 来到这里,简单的加入
this.deferredImportSelectors.add(holder);
}
}

导入刚刚注册的配置

上面一大堆全是解析,然后接下来就要处理这些自动配置了。 回到 parser.parse(candidates); 这个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public void parse(Set<BeanDefinitionHolder> configCandidates) {
for (BeanDefinitionHolder holder : configCandidates) {
BeanDefinition bd = holder.getBeanDefinition();
try {
if (bd instanceof AnnotatedBeanDefinition) {
parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
}
else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) {
parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName());
}
else {
parse(bd.getBeanClassName(), holder.getBeanName());
}
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex);
}
}

// 处理导入配置类(第三方配置)
this.deferredImportSelectorHandler.process();
}

AutoConfigurationImportSelector解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void process() {
// AutoConfigurationImportSelector集合 只有一个元素
List<DeferredImportSelectorHolder> deferredImports = this.deferredImportSelectors;
this.deferredImportSelectors = null;
try {
if (deferredImports != null) {
DeferredImportSelectorGroupingHandler handler = new DeferredImportSelectorGroupingHandler();
deferredImports.sort(DEFERRED_IMPORT_COMPARATOR);
// 注册到 DeferredImportSelectorGroupingHandler 中
deferredImports.forEach(handler::register);
// 重点看这边
handler.processGroupImports();
}
}
finally {
this.deferredImportSelectors = new ArrayList<>();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void processGroupImports() {
for (DeferredImportSelectorGrouping grouping : this.groupings.values()) {
grouping.getImports().forEach(entry -> {
ConfigurationClass configurationClass = this.configurationClasses.get(
entry.getMetadata());
try {
processImports(configurationClass, asSourceClass(configurationClass),
asSourceClasses(entry.getImportClassName()), false);
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to process import candidates for configuration class [" +
configurationClass.getMetadata().getClassName() + "]", ex);
}
});
}
}

先进入这里 grouping.getImports()

1
2
3
4
5
6
7
public Iterable<Group.Entry> getImports() {
for (DeferredImportSelectorHolder deferredImport : this.deferredImports) {
this.group.process(deferredImport.getConfigurationClass().getMetadata(),
deferredImport.getImportSelector());
}
return this.group.selectImports();
}

这个过程就是 SpringBoot 提供的,在类路径提供 additional-spring-configuration-metadata.json 提供导入配置的,然而看起来好像所有的 SpringBoot 模块都在这里了,他只是通过导入类路径的包来判断是否要加载配置。 跳过这一段吧,看着有点累。我们现在只要知道,读取了类路径下的 additional-spring-configuration-metadata.json 后,我们想要看到的 MybatisAutoConfiguration 已经被读取到了就好了。

同样的,spring-boot-mybatis-starter 也提供了上面的注册文件:

好了,接下来回到上面的解析方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void processGroupImports() {
for (DeferredImportSelectorGrouping grouping : this.groupings.values()) {
grouping.getImports().forEach(entry -> {
ConfigurationClass configurationClass = this.configurationClasses.get(
entry.getMetadata());
try {
// 此时进入processImports的时候就不需要再导入配置了
// 直接以 @Configuration 去实现配置
processImports(configurationClass, asSourceClass(configurationClass),
asSourceClasses(entry.getImportClassName()), false);
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to process import candidates for configuration class [" +
configurationClass.getMetadata().getClassName() + "]", ex);
}
});
}
}

那么关于 @Configuration 的解析,在我 之前的文章 就可以看到了。 现在就看看怎么根据主类的所在的包,扫描该包下的所有 Bean

自动包扫描注册Bean

我们得回到 doProcessConfigurationClass 的这个地方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 解析 @ComponentScan,会从主类 WebTestApplication 中拿到 BasePackage
// 然后根据这个 BasePackage 的配置进行扫描下面的类
Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
if (!componentScans.isEmpty() &&
!this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
for (AnnotationAttributes componentScan : componentScans) {
// 通过 componentScanParser 进行解析
Set<BeanDefinitionHolder> scannedBeanDefinitions =
this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
// Check the set of scanned definitions for any further config classes and parse recursively if needed
for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
if (bdCand == null) {
bdCand = holder.getBeanDefinition();
}
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
parse(bdCand.getBeanClassName(), holder.getBeanName());
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, final String declaringClass) {
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(this.registry,
componentScan.getBoolean("useDefaultFilters"), this.environment, this.resourceLoader);

Class<? extends BeanNameGenerator> generatorClass = componentScan.getClass("nameGenerator");
boolean useInheritedGenerator = (BeanNameGenerator.class == generatorClass);
scanner.setBeanNameGenerator(useInheritedGenerator ? this.beanNameGenerator :
BeanUtils.instantiateClass(generatorClass));

ScopedProxyMode scopedProxyMode = componentScan.getEnum("scopedProxy");
if (scopedProxyMode != ScopedProxyMode.DEFAULT) {
scanner.setScopedProxyMode(scopedProxyMode);
}
else {
Class<? extends ScopeMetadataResolver> resolverClass = componentScan.getClass("scopeResolver");
scanner.setScopeMetadataResolver(BeanUtils.instantiateClass(resolverClass));
}

scanner.setResourcePattern(componentScan.getString("resourcePattern"));

for (AnnotationAttributes filter : componentScan.getAnnotationArray("includeFilters")) {
for (TypeFilter typeFilter : typeFiltersFor(filter)) {
scanner.addIncludeFilter(typeFilter);
}
}
for (AnnotationAttributes filter : componentScan.getAnnotationArray("excludeFilters")) {
for (TypeFilter typeFilter : typeFiltersFor(filter)) {
scanner.addExcludeFilter(typeFilter);
}
}

boolean lazyInit = componentScan.getBoolean("lazyInit");
if (lazyInit) {
scanner.getBeanDefinitionDefaults().setLazyInit(true);
}

Set<String> basePackages = new LinkedHashSet<>();
String[] basePackagesArray = componentScan.getStringArray("basePackages");
for (String pkg : basePackagesArray) {
String[] tokenized = StringUtils.tokenizeToStringArray(this.environment.resolvePlaceholders(pkg),
ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
Collections.addAll(basePackages, tokenized);
}
for (Class<?> clazz : componentScan.getClassArray("basePackageClasses")) {
basePackages.add(ClassUtils.getPackageName(clazz));
}
if (basePackages.isEmpty()) {
// 没有指定扫描包,进入这里获取主类的包名
basePackages.add(ClassUtils.getPackageName(declaringClass));
}

scanner.addExcludeFilter(new AbstractTypeHierarchyTraversingFilter(false, false) {
@Override
protected boolean matchClassName(String className) {
return declaringClass.equals(className);
}
});

// 上面基本上都是解析配置的一些东西,过滤器啊,LazyInit等等,到这一步才是真正的扫描
// StringUtils.toStringArray(basePackages) 获取包名
return scanner.doScan(StringUtils.toStringArray(basePackages));
}

真正的扫描,做的事情可谓多得多

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
Assert.notEmpty(basePackages, "At least one base package must be specified");
Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
for (String basePackage : basePackages) {
// 这里又是一个大的模块了,也就是读取ClassPath下所有的文件
Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
// 拿到扫描到的 BeanDefinition 注册到工厂中
for (BeanDefinition candidate : candidates) {
ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
candidate.setScope(scopeMetadata.getScopeName());
String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
if (candidate instanceof AbstractBeanDefinition) {
postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
}
if (candidate instanceof AnnotatedBeanDefinition) {
AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
}
if (checkCandidate(beanName, candidate)) {
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
definitionHolder =
AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
beanDefinitions.add(definitionHolder);
registerBeanDefinition(definitionHolder, this.registry);
}
}
}
return beanDefinitions;
}

扫描:

1
2
3
4
5
6
7
8
public Set<BeanDefinition> findCandidateComponents(String basePackage) {
if (this.componentsIndex != null && indexSupportsIncludeFilters()) {
return addCandidateComponentsFromIndex(this.componentsIndex, basePackage);
}
else {
return scanCandidateComponents(basePackage);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
Set<BeanDefinition> candidates = new LinkedHashSet<>();
try {
String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
resolveBasePackage(basePackage) + '/' + this.resourcePattern;
Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
boolean traceEnabled = logger.isTraceEnabled();
boolean debugEnabled = logger.isDebugEnabled();
// 拿到包下两个文件的 Resource 实例
for (Resource resource : resources) {
if (traceEnabled) {
logger.trace("Scanning " + resource);
}
if (resource.isReadable()) {
try {
MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
if (isCandidateComponent(metadataReader)) {
ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
sbd.setResource(resource);
sbd.setSource(resource);
// 符合某种条件,暂且就当BF中还没存在这个Bean吧
if (isCandidateComponent(sbd)) {
if (debugEnabled) {
logger.debug("Identified candidate component class: " + resource);
}
// 添加
candidates.add(sbd);
}
else {
if (debugEnabled) {
logger.debug("Ignored because not a concrete top-level class: " + resource);
}
}
}
else {
if (traceEnabled) {
logger.trace("Ignored because not matching any filter: " + resource);
}
}
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to read candidate component class: " + resource, ex);
}
}
else {
if (traceEnabled) {
logger.trace("Ignored because not readable: " + resource);
}
}
}
}
catch (IOException ex) {
throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex);
}
// 返回
return candidates;
}

OK,扫描完成,后在 finishBeanFactoryInitialization 进行初始化就可以了

结束

同样的,spring-boot-mybatis-starter 也提供了上面的注册文件:

SpringBoot

OK,现代 Java 开发者应该都对 SpringBoot 很熟悉了吧,一个很 "轻量级"IOC 容器。 我记得 SpringBoot 刚出来的时候,很多博客文章都会说 SpringBoot 减轻了 Java 开发工作者的负担,是个轻量级的框架。然而后面我才发现,并不轻量级。因为,SpringBoot 把需要依赖的东西给封装了起来,但其实比起之前自己控制依赖项目来说,反而会更重了一些,毕竟以前还可以自由组合。现在,SpringBoot 以及框架作者都提供了默认的依赖以及默认的配置,所以说这个框架轻量级其实并不是,要说轻量级应该只是说开发轻量级而已,菜鸟也可以快速上手建立一个后台项目而不必去关心太多项目配置的东西。 框架肯定是一个优秀的框架,我们项目全体也都是 SpringBoot 架构起来的,所以还是需要看看,SpringBoot 偷偷帮我们做了什么事情。

依赖简化

日常使用中,我们只需要引入一个 starter,就可以神奇把我们整合的框架整合起来。但是在以前,我们要使用 MyBatis 的时候,却需要引入 org.mybatis:mybatis 主框架,然后还因为需要整合 Spring 框架,所以我们还需要引入一个 org.mybatis:mybatis-spring。这就算了,这两个包如果版本号对不上,还要出现兼容性的问题。 然而现在我们只需要一个 org.mybatis.spring.boot:mybatis-spring-boot-starter,就可以同时引入这两个包,而且版本号还解决的很OK。所以现在我们可以看看他的 pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot</artifactId>
<version>1.3.1</version>
</parent>
<artifactId>mybatis-spring-boot-starter</artifactId>
<name>mybatis-spring-boot-starter</name>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
</dependency>
</dependencies>
</project>

所以结论是,一个 starter,定义了需要依赖的包的版本,然后通过依赖传递将这些包传递到我们 starter 所在的项目上来。 目前我建立了一个项目,这个项目很简单,只是依赖了一个 spring-boot-starter-web, 我们可以在 idea 上很方便的查看依赖的所有东西:

但其实这个做法在以前 Spring 的时候是已经存在了,这个项目就是 Spring-IO,但我也不知道为什么,可能因为名字取得不够好吧?然后这个项目好像很少人用。或者以前的人觉得会被依赖很多东西进来所以不用这个项目了?

自动装配

我们项目所需要的依赖都弄进来了,下一步就是配置让这些包可以相互配合,共同提供服务呀。 所以,SpringBoot 所提供的第二个功能就是,根据默认的配置,装配依赖进来包里面的 Bean 实例。依赖+装配,就是我们以前一直所需要的操作了,可以说这个框架减少了全世界程序员按 CTRL+C/V 的次数hhhhh。 OK,简单了解的话,我需要一个额外的 starter,这里就使用 org.mybatis.spring.boot:mybatis-spring-boot-starter 来看,仓库地址 项目的目录结构为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
├── LICENSE
├── README.md
├── license.txt
├── mvnw
├── mvnw.cmd
├── mybatis-spring-boot-autoconfigure
│   ├── format.xml
│   ├── license.txt
│   ├── pom.xml
│   └── src
├── mybatis-spring-boot-samples
│   ├── mybatis-spring-boot-sample-annotation
│   ├── mybatis-spring-boot-sample-freemarker
│   ├── mybatis-spring-boot-sample-freemarker-legacy
│   ├── mybatis-spring-boot-sample-groovy
│   ├── mybatis-spring-boot-sample-kotlin
│   ├── mybatis-spring-boot-sample-thymeleaf
│   ├── mybatis-spring-boot-sample-velocity
│   ├── mybatis-spring-boot-sample-velocity-legacy
│   ├── mybatis-spring-boot-sample-war
│   ├── mybatis-spring-boot-sample-web
│   ├── mybatis-spring-boot-sample-xml
│   └── pom.xml
├── mybatis-spring-boot-starter
│   ├── license.txt
│   └── pom.xml
├── mybatis-spring-boot-starter-test
│   └── pom.xml
├── mybatis-spring-boot-test-autoconfigure
│   ├── format.xml
│   ├── pom.xml
│   └── src
├── pom.xml
└── travis
├── after_success.sh
└── settings.xml

其中,mybatis-spring-boot-autoconfigure 比较惹人注目,所以我们现在就看看这个项目。 首先我们看看 main/resources/META-INF/spring.factories,因为 Spring 很喜欢通过这些 meta 文件来促使各个模块很好的解耦但又能彼此配合工作,所以这个文件是定义 自动装配 开始的工厂类:

1
2
3
4
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration,\
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration

那么通过看初步的源码,这两个类都是熟悉的 Java 装配类,那么将会被加入到前面提到的 BeanFactory 容器中,后面解析将会调用里面的配置方法。 第二个优秀的地方就是,定义配置类,就可以在 yaml 文件中提示的出现,可以利用 IDE 工具很好的防止配置名写错。这个类就是 MybatisProperties.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@ConfigurationProperties(prefix = MybatisProperties.MYBATIS_PREFIX)
public class MybatisProperties {

public static final String MYBATIS_PREFIX = "mybatis";

private static final ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();

/**
* Location of MyBatis xml config file.
*/
private String configLocation;

/**
* Locations of MyBatis mapper files.
*/
private String[] mapperLocations;

...
}

OK,我们可以看到,定义了前缀就是 mybatis,而这个类是个贫血型的 Bean,只有属性。这些属性将会贯穿 MyBatis 在项目中整个生命周期。 接下来一个问题,我并不需要每个属性都在 yaml 文件中去定义啊,有些直接使用官方提供的默认值就可以了。 所以官方又贴心的提供了 additional-spring-configuration-metadata.json 这个 JSON 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
{
"properties": [
{
"sourceType": "org.apache.ibatis.session.Configuration",
"defaultValue": "org.apache.ibatis.scripting.xmltags.XMLLanguageDriver",
"name": "mybatis.configuration.default-scripting-language",
"description": "A default LanguageDriver class.",
"type": "java.lang.Class<? extends org.apache.ibatis.scripting.LanguageDriver>",
"deprecation": {
"reason": "Because when this configuration property is used, there is case that custom language driver cannot be registered correctly.",
"replacement": "mybatis.default-scripting-language-driver"
}
},
{
"sourceType": "org.apache.ibatis.session.Configuration",
"defaultValue": "org.apache.ibatis.type.EnumTypeHandler",
"name": "mybatis.configuration.default-enum-type-handler",
"description": "A default TypeHandler class for Enum.",
"type": "java.lang.Class<? extends org.apache.ibatis.type.TypeHandler>"
},
{
"defaultValue": false,
"name": "mybatis.lazy-initialization",
"description": "Set whether enable lazy initialization for mapper bean.",
"type": "java.lang.Boolean"
},
{
"name": "mybatis.scripting-language-driver.velocity.userdirective",
"deprecation": {
"level": "error",
"reason": "The 'userdirective' is deprecated since Velocity 2.x. This property defined for keeping backward compatibility with older velocity version.",
"replacement": "mybatis.scripting-language-driver.velocity.velocity-settings.runtime.custom_directives"
}
}

]
}

不好理解的话,我们直接看第三个默认配置就好了:

1
2
3
4
5
6
{
"defaultValue": false,
"name": "mybatis.lazy-initialization",
"description": "Set whether enable lazy initialization for mapper bean.",
"type": "java.lang.Boolean"
}

看了下图应该明白了吧,上面那个文件就是定义配置的一些说明、默认值的。

所以当我们要造一个框架,又因为很多约定的东西,靠人脑来记已经靠不住的情况下,就可以编写类似于 additional-spring-configuration-metadata.json 这种文件来做约定以及说明了。


OK,上面说完了 SpringBoot 的两个最主要的优点以后,现在就来看看源码。但是源码这块,因为 传递依赖 利用的是 mvn/gradle 的特性,所以 依赖传递 并不需要再说。 那么最主要的就是来解析,怎么自动装配的问题。

简单的SpringBoot项目

pom.xml 依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.BUILD-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.liweidan.web</groupId>
<artifactId>web-test</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>web-test</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

一个主启动器:

1
2
3
4
5
6
7
8
@SpringBootApplication
public class WebTestApplication {

public static void main(String[] args) {
SpringApplication.run(WebTestApplication.class, args);
}

}

一个控制器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
public class UserController {

@GetMapping("users")
public List<Map<String, String>> users() {
List<Map<String, String>> users = new ArrayList<>();
for (int i = 0; i < 3; i++) {
Map<String, String> u = new HashMap<>();
u.put("id", String.valueOf(i));
u.put("name", "name" + i);
users.add(u);
}
return users;
}

}

SpringApplication元信息准备

写过 SpringBoot 项目的同学应该对这个很熟悉了,通过传递一个上下文的根类,SpringBoot 将会自动装载在此类所在的包下面的所有类,并且 args 很明显就是我们在控制台传递的参数,也一并传递给 SpringApplication.run 这个方法,SpringBoot 即可将命令行的参数配置覆盖配置文件指定的配置。 接下来看看这个类做了什么事情:

1
2
3
4
5
6
7
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
return run(new Class<?>[] { primarySource }, args);
}

public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
return new SpringApplication(primarySources).run(args);
}

通过静态方法,传递一个 Class 类作为主资源,然后再传递给主资源数组的 run 方法。那么这个数组其实我们是可以传递多个主资源的,比如我们做项目的时候,想要每个模块包彼此分离,即可传递多个包的主资源路径。 new SpringApplication(primarySources).run(args); 才是真正的进入容器的准备阶段: 首先看看构造器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
// 初始化主资源链表,用于下面需要读取的时候可以遍历.
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
// 判断Web环境,有webFlux/web/普通三个环境,主要通过类路径是否带有相对应需要的类来判断,如果都没有则初始化为普通Java项目.
this.webApplicationType = WebApplicationType.deduceFromClasspath();
// 初始化环境,这个接口多用于web环境,因为需要从web上下文加载一些信息.
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
// 初始化监听器,监听容器生命周期中需要回调的函数
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
// 从运行堆栈中寻找运行这个类的main方法所在的类
this.mainApplicationClass = deduceMainApplicationClass();
}

加载不同模块的元信息

在上面的构造器中看到 setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));Spring 还是跟以前获取 handlers 一样。通过在 META-INF 类加载路径定义不同的 spring.factories,使用类加载器读取这些配置文件资源,解析,加载配置文件中的类,初始化对象,来共同完成业务。

1
2
3
4
5
6
7
8
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
ClassLoader classLoader = getClassLoader();
// Use names and ensure unique to protect against duplicates
Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
AnnotationAwareOrderComparator.sort(instances);
return instances;
}

通过 SpringFactoriesLoader.loadFactoryNames(type, classLoader),加载当前 ClassLoader 中的指定类,源码显示如何读取,后面的 getOrDefault(factoryTypeName, Collections.emptyList()) 还是比较好理解的,如果加载到工厂类的名字就返回不然返回个空集合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
String factoryTypeName = factoryType.getName();
return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
}

private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
// 相当于Map<String, List<String>>结构,先从缓存命中.
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null) {
return result;
}

/*
属性定义了元信息的路径以及名字
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
*/
try {
// 开始加载类路径下特定名字的元信息文件
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
result = new LinkedMultiValueMap<>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
// 通过UrlResource解析成Properties对象
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
// 遍历所有模块的指定文件,然后加入缓存
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryTypeName = ((String) entry.getKey()).trim();
for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
result.add(factoryTypeName, factoryImplementationName.trim());
}
}
}
cache.put(classLoader, result);
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}

启动项目SpringApplication.run

可以看到 run 的源码跟之前的 AbstractApplicationContext#refresh 的味道还是一样的,先加载一系列容器运行时需要的生命周期类(Spring 模块间的也有用户自定义的),然后 refreshContext(context) 刷新容器上下文。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public ConfigurableApplicationContext run(String... args) {
// 这是用来打印加载时间的工具类,暂时可以略过.
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
// 主要设置JVM虚拟机支持无设备情况下让awt可运行的属性
configureHeadlessProperty();
// 一. 加载所有SpringApplicationRunListeners并开始遍历所有Lintener启动监听回调函数
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// 二. 开始准备ConfigurableEnvironment环境
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
// 三. 创建应用上下文
context = createApplicationContext();
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
// 四. 做一些准备工作
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
// 五. 刷新上下文
refreshContext(context);
// 六. 刷新完成后做的一些清理、回调工作
afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
// 七. 启动完成后,调用所有SpringApplicationRunListener的完成启动的回调函数
listeners.started(context);
// 八. 主要处理ApplicationRunner和CommandLineRunner的回调
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}

try {
// 九. 运行时的SpringApplicationRunListener回调函数
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
return context;
}

装载SpringApplicationRunListener

1
2
3
4
5
private SpringApplicationRunListeners getRunListeners(String[] args) {
Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
return new SpringApplicationRunListeners(logger,
getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args));
}

首先我们看第一步,加载所有的 SpringApplicationRunListener 子类,这个加载方式跟上面所说的加载元信息是一样的,只不过指定加载 SpringApplicationRunListener.class 类。 我们知道,Spring 经常会定义很多生命周期回调,供用户根据需求切入框架。这次 SpringBoot 的生命周期是 SpringApplicationRunListener。 首先看看 SpringApplicationRunListener 定义哪些生命周期函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public interface SpringApplicationRunListener {

// 项目开始时调用
default void starting() {
}

// 环境准备好时
default void environmentPrepared(ConfigurableEnvironment environment) {
}

// 上下文准备好时
default void contextPrepared(ConfigurableApplicationContext context) {
}

// 上下文读取完成
default void contextLoaded(ConfigurableApplicationContext context) {
}

// 启动完成
default void started(ConfigurableApplicationContext context) {
}

// 项目运行时调用
default void running(ConfigurableApplicationContext context) {
}

// 项目失败时
default void failed(ConfigurableApplicationContext context, Throwable exception) {
}

}

准备ConfigurableEnvironment环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments) {
// 首先根据环境类型获取对象的配置实现对象
ConfigurableEnvironment environment = getOrCreateEnvironment();
// 配置ConversionService单例对象,以及判断有没有设置Profiles环境
configureEnvironment(environment, applicationArguments.getSourceArgs());
// 读取其他环境中(比如web.xml)读取的配置属性
ConfigurationPropertySources.attach(environment);
// 继续上面的SpringApplicationRunListener生命周期调用
listeners.environmentPrepared(environment);
// 把环境绑定到Binder中,Binder是Spring提供的一个记录对象的容器
bindToSpringApplication(environment);
if (!this.isCustomEnvironment) {
// 如果当前的配置环境和重新判断的环境不同,则转换成当前的环境(有可能在生命周期修改了加载的环境?)
environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
deduceEnvironmentClass());
}
// 重新读取特定环境的配置
ConfigurationPropertySources.attach(environment);
return environment;
}

private ConfigurableEnvironment getOrCreateEnvironment() {
if (this.environment != null) {
return this.environment;
}
switch (this.webApplicationType) {
case SERVLET:
return new StandardServletEnvironment();
case REACTIVE:
return new StandardReactiveWebEnvironment();
default:
return new StandardEnvironment();
}
}

然后在主线程中配置忽略 BooleanBean

1
2
3
4
5
6
7
8
9
10
11
12
13
public ConfigurableApplicationContext run(String... args) {
//...
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);
//...
}

private void configureIgnoreBeanInfo(ConfigurableEnvironment environment) {
if (System.getProperty(CachedIntrospectionResults.IGNORE_BEANINFO_PROPERTY_NAME) == null) {
Boolean ignore = environment.getProperty("spring.beaninfo.ignore", Boolean.class, Boolean.TRUE);
System.setProperty(CachedIntrospectionResults.IGNORE_BEANINFO_PROPERTY_NAME, ignore.toString());
}
}

创建上下文

1
2
3
4
context = createApplicationContext();
// 获取模块的错误解释器,用于项目启动的时候解析是什么错误
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);

根据不同环境创建不同的上下文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
protected ConfigurableApplicationContext createApplicationContext() {
Class<?> contextClass = this.applicationContextClass;
if (contextClass == null) {
try {
switch (this.webApplicationType) {
case SERVLET:
contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);
break;
case REACTIVE:
contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
break;
default:
contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
}
}
catch (ClassNotFoundException ex) {
throw new IllegalStateException(
"Unable create a default ApplicationContext, please specify an ApplicationContextClass", ex);
}
}
return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
}

Context准备工作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment,
SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
// 设置上面准备好的环境信息
context.setEnvironment(environment);
// 处理之前做一些事情,但是当前环境下,只是注册了一个 ConversionService 到 BeanFactory 中
postProcessApplicationContext(context);
// 调用之前注册的所有Initializers
applyInitializers(context);
// 再调用所有的listeners
listeners.contextPrepared(context);
if (this.logStartupInfo) {
logStartupInfo(context.getParent() == null);
logStartupProfileInfo(context);
}
// 注册SpringBoot独有的一些Bean
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
if (printedBanner != null) {
beanFactory.registerSingleton("springBootBanner", printedBanner);
}
// 接下来跟之前一样
if (beanFactory instanceof DefaultListableBeanFactory) {
((DefaultListableBeanFactory) beanFactory)
.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
}
if (this.lazyInitialization) {
context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
}
// Load the sources
Set<Object> sources = getAllSources();
Assert.notEmpty(sources, "Sources must not be empty");
// 根据我们传递的主类进行读取,放入BeanFactory中
load(context, sources.toArray(new Object[0]));
listeners.contextLoaded(context);
}

其实当前 postProcessApplicationContext 除了注册 ConversionService 以外其他事情都没做。

refreshContext刷新上下文

因为我要看看自动装配的问题,所以这个时候为了故事比较好说,我加了 mybatis-spring-boot-starter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.BUILD-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.liweidan.web</groupId>
<artifactId>web-test</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>web-test</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.197</version>
</dependency>

</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

刷新上下文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void refreshContext(ConfigurableApplicationContext context) {
refresh(context);
if (this.registerShutdownHook) {
try {
context.registerShutdownHook();
}
catch (AccessControlException ex) {
// Not allowed in some environments.
}
}
}
protected void refresh(ApplicationContext applicationContext) {
Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext);
((AbstractApplicationContext) applicationContext).refresh();
}

然后接下来就去到我们熟悉的 AbstractApplicationContext 中,重温一下这个 refresh() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
prepareRefresh();

// Tell the subclass to refresh the internal bean factory.
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);

try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);

// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);

// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);

// Initialize message source for this context.
initMessageSource();

// Initialize event multicaster for this context.
initApplicationEventMulticaster();

// Initialize other special beans in specific context subclasses.
onRefresh();

// Check for listener beans and register them.
registerListeners();

// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);

// Last step: publish corresponding event.
finishRefresh();
}

catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}

// Destroy already created singletons to avoid dangling resources.
destroyBeans();

// Reset 'active' flag.
cancelRefresh(ex);

// Propagate exception to caller.
throw ex;
}

finally {
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
resetCommonCaches();
}
}
}

同之前一样,在 invokeBeanFactoryPostProcessors(beanFactory) 这句话出现了 BeanDefinition 数量的剧增,那么我们可以推断出,这句话还是关键,就是在这句话开始解析我们的项目依赖。

其实按照我的猜测,自动配置应该是 starter 提供了一些配置类交给 SpringBoot 注入 Spring 的 BeanFactory 中,但是现在看来,好像 SpringBoot 就不需要做什么东西,直接委托给容器扫描解析执行了。

invokeBeanFactoryPostProcessors

1
2
3
4
5
6
7
8
9
10
11
protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
// 利用委派模式委托给 PostProcessorRegistrationDelegate 进行执行
PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());

// Detect a LoadTimeWeaver and prepare for weaving, if found in the meantime
// (e.g. through an @Bean method registered by ConfigurationClassPostProcessor)
if (beanFactory.getTempClassLoader() == null && beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) {
beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));
beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
}
}

PostProcessorRegistrationDelegate委派对象

这是一个在 Context 模块下对象,主要处理 PostProcessor 相关的事情,看到签名只提供两个方法:

  • invokeBeanFactoryPostProcessors 执行 BF 的 PostProcessors
  • registerBeanPostProcessors 注册 PostProcessors 到 BF 中

关于普通的配置类解析,之前在 chapters3_高级的Beafctory—Spring上下文 已经有提到,解析项目中配置类不明白的话可以先看看那篇文章。 不过我们现在重点是怎么读取到 starter 框架里边的自动配置信息:

1
2
3
4
5
6
7
private static void invokeBeanDefinitionRegistryPostProcessors(
Collection<? extends BeanDefinitionRegistryPostProcessor> postProcessors, BeanDefinitionRegistry registry) {

for (BeanDefinitionRegistryPostProcessor postProcessor : postProcessors) {
postProcessor.postProcessBeanDefinitionRegistry(registry);
}
}

当前 postProcessors 里边只有一个 ConfigurationClassPostProcessor 那我们进入对应的 postProcessBeanDefinitionRegistry 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
int registryId = System.identityHashCode(registry);
if (this.registriesPostProcessed.contains(registryId)) {
throw new IllegalStateException(
"postProcessBeanDefinitionRegistry already called on this post-processor against " + registry);
}
if (this.factoriesPostProcessed.contains(registryId)) {
throw new IllegalStateException(
"postProcessBeanFactory already called on this post-processor against " + registry);
}
this.registriesPostProcessed.add(registryId);

processConfigBeanDefinitions(registry);
}

processConfigBeanDefinitions 解析配置

由于我们现在需要跟自动导入的配置,所以我们应该需要跟的类配置是我们的主类,也就是 WebTestApplication 这个类,这个类上修饰的注解 @SpringBootApplication 是自动注入的关键

这个才是执行自动配置的重点之重:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
/**
* Build and validate a configuration model based on the registry of
* {@link Configuration} classes.
*/
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
List<BeanDefinitionHolder> configCandidates = new ArrayList<>();
String[] candidateNames = registry.getBeanDefinitionNames();

for (String beanName : candidateNames) {
BeanDefinition beanDef = registry.getBeanDefinition(beanName);
if (beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE) != null) {
if (logger.isDebugEnabled()) {
logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);
}
}
else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
}
}

// Return immediately if no @Configuration classes were found
if (configCandidates.isEmpty()) {
return;
}

// Sort by previously determined @Order value, if applicable
configCandidates.sort((bd1, bd2) -> {
int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition());
int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition());
return Integer.compare(i1, i2);
});

// Detect any custom bean name generation strategy supplied through the enclosing application context
SingletonBeanRegistry sbr = null;
if (registry instanceof SingletonBeanRegistry) {
sbr = (SingletonBeanRegistry) registry;
if (!this.localBeanNameGeneratorSet) {
BeanNameGenerator generator = (BeanNameGenerator) sbr.getSingleton(
AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR);
if (generator != null) {
this.componentScanBeanNameGenerator = generator;
this.importBeanNameGenerator = generator;
}
}
}

if (this.environment == null) {
this.environment = new StandardEnvironment();
}

// Parse each @Configuration class
ConfigurationClassParser parser = new ConfigurationClassParser(
this.metadataReaderFactory, this.problemReporter, this.environment,
this.resourceLoader, this.componentScanBeanNameGenerator, registry);

Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());
// ↑ 这上面属于解析开始的准备工作 ↑
do {
parser.parse(candidates);
parser.validate();

Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
configClasses.removeAll(alreadyParsed);

// Read the model and create bean definitions based on its content
if (this.reader == null) {
this.reader = new ConfigurationClassBeanDefinitionReader(
registry, this.sourceExtractor, this.resourceLoader, this.environment,
this.importBeanNameGenerator, parser.getImportRegistry());
}
this.reader.loadBeanDefinitions(configClasses);
alreadyParsed.addAll(configClasses);

candidates.clear();
if (registry.getBeanDefinitionCount() > candidateNames.length) {
String[] newCandidateNames = registry.getBeanDefinitionNames();
Set<String> oldCandidateNames = new HashSet<>(Arrays.asList(candidateNames));
Set<String> alreadyParsedClasses = new HashSet<>();
for (ConfigurationClass configurationClass : alreadyParsed) {
alreadyParsedClasses.add(configurationClass.getMetadata().getClassName());
}
for (String candidateName : newCandidateNames) {
if (!oldCandidateNames.contains(candidateName)) {
BeanDefinition bd = registry.getBeanDefinition(candidateName);
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, this.metadataReaderFactory) &&
!alreadyParsedClasses.contains(bd.getBeanClassName())) {
candidates.add(new BeanDefinitionHolder(bd, candidateName));
}
}
}
candidateNames = newCandidateNames;
}
}
while (!candidates.isEmpty());

// Register the ImportRegistry as a bean in order to support ImportAware @Configuration classes
if (sbr != null && !sbr.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) {
sbr.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry());
}

if (this.metadataReaderFactory instanceof CachingMetadataReaderFactory) {
// Clear cache in externally provided MetadataReaderFactory; this is a no-op
// for a shared cache since it'll be cleared by the ApplicationContext.
((CachingMetadataReaderFactory) this.metadataReaderFactory).clearCache();
}
}

重点看看 parser.parse(candidates); 这句话是怎么解析的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public void parse(Set<BeanDefinitionHolder> configCandidates) {
for (BeanDefinitionHolder holder : configCandidates) {
BeanDefinition bd = holder.getBeanDefinition();
try {
if (bd instanceof AnnotatedBeanDefinition) {
// 因为这是一个使用注解的Bean,所以应该关注这里
parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
}
else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) {
parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName());
}
else {
parse(bd.getBeanClassName(), holder.getBeanName());
}
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex);
}
}

this.deferredImportSelectorHandler.process();
}

protected final void parse(AnnotationMetadata metadata, String beanName) throws IOException {
processConfigurationClass(new ConfigurationClass(metadata, beanName));
}

protected void processConfigurationClass(ConfigurationClass configClass) throws IOException {
// 如果当前的类是一个解析类,则跳过解析
if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {
return;
}

// 判断是否已经解析过了
ConfigurationClass existingClass = this.configurationClasses.get(configClass);
if (existingClass != null) {
if (configClass.isImported()) {
if (existingClass.isImported()) {
existingClass.mergeImportedBy(configClass);
}
// Otherwise ignore new imported config class; existing non-imported class overrides it.
return;
}
else {
// Explicit bean definition found, probably replacing an import.
// Let's remove the old one and go with the new one.
this.configurationClasses.remove(configClass);
this.knownSuperclasses.values().removeIf(configClass::equals);
}
}

// 递归解析配置类以及他的父类、接口等等
SourceClass sourceClass = asSourceClass(configClass);
do {
sourceClass = doProcessConfigurationClass(configClass, sourceClass);
}
while (sourceClass != null);

this.configurationClasses.put(configClass, configClass);
}

然后我们要进入 doProcessConfigurationClass 观察解析的过程。 这串代码其实有点长……:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
@Nullable
protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)
throws IOException {

if (configClass.getMetadata().isAnnotated(Component.class.getName())) {
// Recursively process any member (nested) classes first
processMemberClasses(configClass, sourceClass);
}

// 解析需要导入Property配置文件的类
for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), PropertySources.class,
org.springframework.context.annotation.PropertySource.class)) {
if (this.environment instanceof ConfigurableEnvironment) {
processPropertySource(propertySource);
}
else {
logger.info("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +
"]. Reason: Environment must implement ConfigurableEnvironment");
}
}

// 解析 @ComponentScan
Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
if (!componentScans.isEmpty() &&
!this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
for (AnnotationAttributes componentScan : componentScans) {
// The config class is annotated with @ComponentScan -> perform the scan immediately
Set<BeanDefinitionHolder> scannedBeanDefinitions =
this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
// Check the set of scanned definitions for any further config classes and parse recursively if needed
for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
if (bdCand == null) {
bdCand = holder.getBeanDefinition();
}
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
parse(bdCand.getBeanClassName(), holder.getBeanName());
}
}
}
}

// 解析 @Import 的类,这句话是重点
processImports(configClass, sourceClass, getImports(sourceClass), true);

// Process any @ImportResource annotations
AnnotationAttributes importResource =
AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class);
if (importResource != null) {
String[] resources = importResource.getStringArray("locations");
Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader");
for (String resource : resources) {
String resolvedResource = this.environment.resolveRequiredPlaceholders(resource);
configClass.addImportedResource(resolvedResource, readerClass);
}
}

// 解析 @Bean 方法
Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);
for (MethodMetadata methodMetadata : beanMethods) {
configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
}

// 解析接口默认方法
processInterfaces(configClass, sourceClass);

// 如果有父类,返回父类,由上一层继续调用这个方法继续解析
if (sourceClass.getMetadata().hasSuperClass()) {
String superclass = sourceClass.getMetadata().getSuperClassName();
if (superclass != null && !superclass.startsWith("java") &&
!this.knownSuperclasses.containsKey(superclass)) {
this.knownSuperclasses.put(superclass, configClass);
// Superclass found, return its annotation metadata and recurse
return sourceClass.getSuperClass();
}
}

// 返回Null表示解析结束
return null;
}

好了,可以看到通过 Java 类的解析可谓覆盖到尼玛我想象不到的地方….但他们就是应该要有 挑重点来看吧,我的主类没有父级也没有接口,直接重点看 processImports(configClass, sourceClass, getImports(sourceClass), true) 这句话。 这句话首先需要获取所有的导入配置 getImports(sourceClass)

1
2
3
4
5
6
private Set<SourceClass> getImports(SourceClass sourceClass) throws IOException {
Set<SourceClass> imports = new LinkedHashSet<>();
Set<SourceClass> visited = new LinkedHashSet<>();
collectImports(sourceClass, imports, visited);
return imports;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
private void collectImports(SourceClass sourceClass, Set<SourceClass> imports, Set<SourceClass> visited)
throws IOException {

if (visited.add(sourceClass)) {
for (SourceClass annotation : sourceClass.getAnnotations()) {
String annName = annotation.getMetadata().getClassName();
if (!annName.equals(Import.class.getName())) {
collectImports(annotation, imports, visited);
}
}
imports.addAll(sourceClass.getAnnotationAttributes(Import.class.getName(), "value"));
}
}

这两个东西,切忌不要去 debug 一个一个看….我在这里看晕了好几个小时,应该是条件断点判断 sourceClass.getAnnotationAttributes(Import.class.getName(), "value").size() > 0 再停住

好了等到断点停住的时候,发现是 @interface SpringBootApplication > @interface EnableAutoConfiguration 上面的 @Import(AutoConfigurationImportSelector.class) 在这里导入了。以及这个类上边的 @AutoConfigurationPackage 导入了 @Import(AutoConfigurationPackages.Registrar.class) 所以当前需要解析的 importCandidatesAutoConfigurationPackages.Registrar.class 以及 AutoConfigurationImportSelector.class 两个类。 那么这两个类是什么用的,我现在也还不知道…先知道他被导入就好了,接下去看 processImports 方法了,这个方法应该有答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
Collection<SourceClass> importCandidates, boolean checkForCircularImports) {

if (importCandidates.isEmpty()) {
return;
}

// 由上面传递的 checkForCircularImports 参数决定是否判断有没有循环引入
if (checkForCircularImports && isChainedImportOnStack(configClass)) {
this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
}
else {
this.importStack.push(configClass);
try {
// 开始解析导入类
for (SourceClass candidate : importCandidates) {
// 第一层,根据不同的导入类型进行解析
// AutoConfigurationImportSelector 来到这个分支
if (candidate.isAssignable(ImportSelector.class)) {
// Candidate class is an ImportSelector -> delegate to it to determine imports
Class<?> candidateClass = candidate.loadClass();
ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class,
this.environment, this.resourceLoader, this.registry);
if (selector instanceof DeferredImportSelector) {
this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
}
else {
String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames);
// 递归解析
processImports(configClass, currentSourceClass, importSourceClasses, false);
}
}
// AutoConfigurationPackages.Registrar进入这个分支
else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
// Candidate class is an ImportBeanDefinitionRegistrar ->
// delegate to it to register additional bean definitions
Class<?> candidateClass = candidate.loadClass();
ImportBeanDefinitionRegistrar registrar =
ParserStrategyUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class,
this.environment, this.resourceLoader, this.registry);
configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
}
else {
// 直接以@Configuration的方式进行处理
this.importStack.registerImport(
currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
processConfigurationClass(candidate.asConfigClass(configClass));
}
}
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to process import candidates for configuration class [" +
configClass.getMetadata().getClassName() + "]", ex);
}
finally {
this.importStack.pop();
}
}
}

不行呀,因为需要根据类型来做,先给两个类的类型吧:

这三个分支对应着三个不同的配置类型:

  1. ImportSelector 用于导入配置类的信息
  2. Registrar 用于解析自定义注解动态生成 Bean 的信息
  3. @Configuration 读取到配置类,直接解析

那我们就先按照上图的顺序来解析这些配置类吧,首先是 AutoConfigurationPackages.Registrar 对象的解析。我先从上面一大串截取代码片段来看:

1
2
3
4
5
6
7
8
9
10
11
// ...
else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
// 额外注册BeanDefinition用
Class<?> candidateClass = candidate.loadClass();
ImportBeanDefinitionRegistrar registrar =
ParserStrategyUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class,
this.environment, this.resourceLoader, this.registry);
// 简单的添加(导入)到当前的 configClass 中
configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
}
// ...

先放到后面,应该是解析的时候会调用到。 接下来看第二个 AutoConfigurationImportSelector.class 的解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (candidate.isAssignable(ImportSelector.class)) {
// Candidate class is an ImportSelector -> delegate to it to determine imports
Class<?> candidateClass = candidate.loadClass();
ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class,
this.environment, this.resourceLoader, this.registry);
if (selector instanceof DeferredImportSelector) {
// 进入这里
this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
}
else {
String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames);
processImports(configClass, currentSourceClass, importSourceClasses, false);
}
}

这一块比较麻烦,在上面 uml 可以看到,AutoConfigurationImportSelector.class 属于 DeferredImportSelector,所以交给 deferredImportSelectorHandler 进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
public void handle(ConfigurationClass configClass, DeferredImportSelector importSelector) {
DeferredImportSelectorHolder holder = new DeferredImportSelectorHolder(
configClass, importSelector);
if (this.deferredImportSelectors == null) {
DeferredImportSelectorGroupingHandler handler = new DeferredImportSelectorGroupingHandler();
handler.register(holder);
handler.processGroupImports();
}
else {
// 来到这里,简单的加入
this.deferredImportSelectors.add(holder);
}
}

导入刚刚注册的配置

上面一大堆全是解析,然后接下来就要处理这些自动配置了。 回到 parser.parse(candidates); 这个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public void parse(Set<BeanDefinitionHolder> configCandidates) {
for (BeanDefinitionHolder holder : configCandidates) {
BeanDefinition bd = holder.getBeanDefinition();
try {
if (bd instanceof AnnotatedBeanDefinition) {
parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
}
else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) {
parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName());
}
else {
parse(bd.getBeanClassName(), holder.getBeanName());
}
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex);
}
}

// 处理导入配置类(第三方配置)
this.deferredImportSelectorHandler.process();
}

AutoConfigurationImportSelector解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void process() {
// AutoConfigurationImportSelector集合 只有一个元素
List<DeferredImportSelectorHolder> deferredImports = this.deferredImportSelectors;
this.deferredImportSelectors = null;
try {
if (deferredImports != null) {
DeferredImportSelectorGroupingHandler handler = new DeferredImportSelectorGroupingHandler();
deferredImports.sort(DEFERRED_IMPORT_COMPARATOR);
// 注册到 DeferredImportSelectorGroupingHandler 中
deferredImports.forEach(handler::register);
// 重点看这边
handler.processGroupImports();
}
}
finally {
this.deferredImportSelectors = new ArrayList<>();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void processGroupImports() {
for (DeferredImportSelectorGrouping grouping : this.groupings.values()) {
grouping.getImports().forEach(entry -> {
ConfigurationClass configurationClass = this.configurationClasses.get(
entry.getMetadata());
try {
processImports(configurationClass, asSourceClass(configurationClass),
asSourceClasses(entry.getImportClassName()), false);
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to process import candidates for configuration class [" +
configurationClass.getMetadata().getClassName() + "]", ex);
}
});
}
}

先进入这里 grouping.getImports()

1
2
3
4
5
6
7
public Iterable<Group.Entry> getImports() {
for (DeferredImportSelectorHolder deferredImport : this.deferredImports) {
this.group.process(deferredImport.getConfigurationClass().getMetadata(),
deferredImport.getImportSelector());
}
return this.group.selectImports();
}

这个过程就是 SpringBoot 提供的,在类路径提供 additional-spring-configuration-metadata.json 提供导入配置的,然而看起来好像所有的 SpringBoot 模块都在这里了,他只是通过导入类路径的包来判断是否要加载配置。 跳过这一段吧,看着有点累。我们现在只要知道,读取了类路径下的 additional-spring-configuration-metadata.json 后,我们想要看到的 MybatisAutoConfiguration 已经被读取到了就好了。

同样的,spring-boot-mybatis-starter 也提供了上面的注册文件:

好了,接下来回到上面的解析方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void processGroupImports() {
for (DeferredImportSelectorGrouping grouping : this.groupings.values()) {
grouping.getImports().forEach(entry -> {
ConfigurationClass configurationClass = this.configurationClasses.get(
entry.getMetadata());
try {
// 此时进入processImports的时候就不需要再导入配置了
// 直接以 @Configuration 去实现配置
processImports(configurationClass, asSourceClass(configurationClass),
asSourceClasses(entry.getImportClassName()), false);
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to process import candidates for configuration class [" +
configurationClass.getMetadata().getClassName() + "]", ex);
}
});
}
}

那么关于 @Configuration 的解析,在我 之前的文章 就可以看到了。 现在就看看怎么根据主类的所在的包,扫描该包下的所有 Bean

自动包扫描注册Bean

我们得回到 doProcessConfigurationClass 的这个地方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 解析 @ComponentScan,会从主类 WebTestApplication 中拿到 BasePackage
// 然后根据这个 BasePackage 的配置进行扫描下面的类
Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
if (!componentScans.isEmpty() &&
!this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
for (AnnotationAttributes componentScan : componentScans) {
// 通过 componentScanParser 进行解析
Set<BeanDefinitionHolder> scannedBeanDefinitions =
this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
// Check the set of scanned definitions for any further config classes and parse recursively if needed
for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
if (bdCand == null) {
bdCand = holder.getBeanDefinition();
}
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
parse(bdCand.getBeanClassName(), holder.getBeanName());
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, final String declaringClass) {
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(this.registry,
componentScan.getBoolean("useDefaultFilters"), this.environment, this.resourceLoader);

Class<? extends BeanNameGenerator> generatorClass = componentScan.getClass("nameGenerator");
boolean useInheritedGenerator = (BeanNameGenerator.class == generatorClass);
scanner.setBeanNameGenerator(useInheritedGenerator ? this.beanNameGenerator :
BeanUtils.instantiateClass(generatorClass));

ScopedProxyMode scopedProxyMode = componentScan.getEnum("scopedProxy");
if (scopedProxyMode != ScopedProxyMode.DEFAULT) {
scanner.setScopedProxyMode(scopedProxyMode);
}
else {
Class<? extends ScopeMetadataResolver> resolverClass = componentScan.getClass("scopeResolver");
scanner.setScopeMetadataResolver(BeanUtils.instantiateClass(resolverClass));
}

scanner.setResourcePattern(componentScan.getString("resourcePattern"));

for (AnnotationAttributes filter : componentScan.getAnnotationArray("includeFilters")) {
for (TypeFilter typeFilter : typeFiltersFor(filter)) {
scanner.addIncludeFilter(typeFilter);
}
}
for (AnnotationAttributes filter : componentScan.getAnnotationArray("excludeFilters")) {
for (TypeFilter typeFilter : typeFiltersFor(filter)) {
scanner.addExcludeFilter(typeFilter);
}
}

boolean lazyInit = componentScan.getBoolean("lazyInit");
if (lazyInit) {
scanner.getBeanDefinitionDefaults().setLazyInit(true);
}

Set<String> basePackages = new LinkedHashSet<>();
String[] basePackagesArray = componentScan.getStringArray("basePackages");
for (String pkg : basePackagesArray) {
String[] tokenized = StringUtils.tokenizeToStringArray(this.environment.resolvePlaceholders(pkg),
ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
Collections.addAll(basePackages, tokenized);
}
for (Class<?> clazz : componentScan.getClassArray("basePackageClasses")) {
basePackages.add(ClassUtils.getPackageName(clazz));
}
if (basePackages.isEmpty()) {
// 没有指定扫描包,进入这里获取主类的包名
basePackages.add(ClassUtils.getPackageName(declaringClass));
}

scanner.addExcludeFilter(new AbstractTypeHierarchyTraversingFilter(false, false) {
@Override
protected boolean matchClassName(String className) {
return declaringClass.equals(className);
}
});

// 上面基本上都是解析配置的一些东西,过滤器啊,LazyInit等等,到这一步才是真正的扫描
// StringUtils.toStringArray(basePackages) 获取包名
return scanner.doScan(StringUtils.toStringArray(basePackages));
}

真正的扫描,做的事情可谓多得多

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
Assert.notEmpty(basePackages, "At least one base package must be specified");
Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
for (String basePackage : basePackages) {
// 这里又是一个大的模块了,也就是读取ClassPath下所有的文件
Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
// 拿到扫描到的 BeanDefinition 注册到工厂中
for (BeanDefinition candidate : candidates) {
ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
candidate.setScope(scopeMetadata.getScopeName());
String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
if (candidate instanceof AbstractBeanDefinition) {
postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
}
if (candidate instanceof AnnotatedBeanDefinition) {
AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
}
if (checkCandidate(beanName, candidate)) {
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
definitionHolder =
AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
beanDefinitions.add(definitionHolder);
registerBeanDefinition(definitionHolder, this.registry);
}
}
}
return beanDefinitions;
}

扫描:

1
2
3
4
5
6
7
8
public Set<BeanDefinition> findCandidateComponents(String basePackage) {
if (this.componentsIndex != null && indexSupportsIncludeFilters()) {
return addCandidateComponentsFromIndex(this.componentsIndex, basePackage);
}
else {
return scanCandidateComponents(basePackage);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
Set<BeanDefinition> candidates = new LinkedHashSet<>();
try {
String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
resolveBasePackage(basePackage) + '/' + this.resourcePattern;
Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
boolean traceEnabled = logger.isTraceEnabled();
boolean debugEnabled = logger.isDebugEnabled();
// 拿到包下两个文件的 Resource 实例
for (Resource resource : resources) {
if (traceEnabled) {
logger.trace("Scanning " + resource);
}
if (resource.isReadable()) {
try {
MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
if (isCandidateComponent(metadataReader)) {
ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
sbd.setResource(resource);
sbd.setSource(resource);
// 符合某种条件,暂且就当BF中还没存在这个Bean吧
if (isCandidateComponent(sbd)) {
if (debugEnabled) {
logger.debug("Identified candidate component class: " + resource);
}
// 添加
candidates.add(sbd);
}
else {
if (debugEnabled) {
logger.debug("Ignored because not a concrete top-level class: " + resource);
}
}
}
else {
if (traceEnabled) {
logger.trace("Ignored because not matching any filter: " + resource);
}
}
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to read candidate component class: " + resource, ex);
}
}
else {
if (traceEnabled) {
logger.trace("Ignored because not readable: " + resource);
}
}
}
}
catch (IOException ex) {
throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex);
}
// 返回
return candidates;
}

OK,扫描完成,后在 finishBeanFactoryInitialization 进行初始化就可以了

结束 好了,接下来回到上面的解析方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void processGroupImports() {
for (DeferredImportSelectorGrouping grouping : this.groupings.values()) {
grouping.getImports().forEach(entry -> {
ConfigurationClass configurationClass = this.configurationClasses.get(
entry.getMetadata());
try {
// 此时进入processImports的时候就不需要再导入配置了
// 直接以 @Configuration 去实现配置
processImports(configurationClass, asSourceClass(configurationClass),
asSourceClasses(entry.getImportClassName()), false);
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to process import candidates for configuration class [" +
configurationClass.getMetadata().getClassName() + "]", ex);
}
});
}
}

那么关于 @Configuration 的解析,在我 之前的文章 就可以看到了。 现在就看看怎么根据主类的所在的包,扫描该包下的所有 Bean

自动包扫描注册Bean

我们得回到 doProcessConfigurationClass 的这个地方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 解析 @ComponentScan,会从主类 WebTestApplication 中拿到 BasePackage
// 然后根据这个 BasePackage 的配置进行扫描下面的类
Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
if (!componentScans.isEmpty() &&
!this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
for (AnnotationAttributes componentScan : componentScans) {
// 通过 componentScanParser 进行解析
Set<BeanDefinitionHolder> scannedBeanDefinitions =
this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
// Check the set of scanned definitions for any further config classes and parse recursively if needed
for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
if (bdCand == null) {
bdCand = holder.getBeanDefinition();
}
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
parse(bdCand.getBeanClassName(), holder.getBeanName());
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, final String declaringClass) {
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(this.registry,
componentScan.getBoolean("useDefaultFilters"), this.environment, this.resourceLoader);

Class<? extends BeanNameGenerator> generatorClass = componentScan.getClass("nameGenerator");
boolean useInheritedGenerator = (BeanNameGenerator.class == generatorClass);
scanner.setBeanNameGenerator(useInheritedGenerator ? this.beanNameGenerator :
BeanUtils.instantiateClass(generatorClass));

ScopedProxyMode scopedProxyMode = componentScan.getEnum("scopedProxy");
if (scopedProxyMode != ScopedProxyMode.DEFAULT) {
scanner.setScopedProxyMode(scopedProxyMode);
}
else {
Class<? extends ScopeMetadataResolver> resolverClass = componentScan.getClass("scopeResolver");
scanner.setScopeMetadataResolver(BeanUtils.instantiateClass(resolverClass));
}

scanner.setResourcePattern(componentScan.getString("resourcePattern"));

for (AnnotationAttributes filter : componentScan.getAnnotationArray("includeFilters")) {
for (TypeFilter typeFilter : typeFiltersFor(filter)) {
scanner.addIncludeFilter(typeFilter);
}
}
for (AnnotationAttributes filter : componentScan.getAnnotationArray("excludeFilters")) {
for (TypeFilter typeFilter : typeFiltersFor(filter)) {
scanner.addExcludeFilter(typeFilter);
}
}

boolean lazyInit = componentScan.getBoolean("lazyInit");
if (lazyInit) {
scanner.getBeanDefinitionDefaults().setLazyInit(true);
}

Set<String> basePackages = new LinkedHashSet<>();
String[] basePackagesArray = componentScan.getStringArray("basePackages");
for (String pkg : basePackagesArray) {
String[] tokenized = StringUtils.tokenizeToStringArray(this.environment.resolvePlaceholders(pkg),
ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
Collections.addAll(basePackages, tokenized);
}
for (Class<?> clazz : componentScan.getClassArray("basePackageClasses")) {
basePackages.add(ClassUtils.getPackageName(clazz));
}
if (basePackages.isEmpty()) {
// 没有指定扫描包,进入这里获取主类的包名
basePackages.add(ClassUtils.getPackageName(declaringClass));
}

scanner.addExcludeFilter(new AbstractTypeHierarchyTraversingFilter(false, false) {
@Override
protected boolean matchClassName(String className) {
return declaringClass.equals(className);
}
});

// 上面基本上都是解析配置的一些东西,过滤器啊,LazyInit等等,到这一步才是真正的扫描
// StringUtils.toStringArray(basePackages) 获取包名
return scanner.doScan(StringUtils.toStringArray(basePackages));
}

真正的扫描,做的事情可谓多得多

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
Assert.notEmpty(basePackages, "At least one base package must be specified");
Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
for (String basePackage : basePackages) {
// 这里又是一个大的模块了,也就是读取ClassPath下所有的文件
Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
// 拿到扫描到的 BeanDefinition 注册到工厂中
for (BeanDefinition candidate : candidates) {
ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
candidate.setScope(scopeMetadata.getScopeName());
String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
if (candidate instanceof AbstractBeanDefinition) {
postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
}
if (candidate instanceof AnnotatedBeanDefinition) {
AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
}
if (checkCandidate(beanName, candidate)) {
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
definitionHolder =
AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
beanDefinitions.add(definitionHolder);
registerBeanDefinition(definitionHolder, this.registry);
}
}
}
return beanDefinitions;
}

扫描:

1
2
3
4
5
6
7
8
public Set<BeanDefinition> findCandidateComponents(String basePackage) {
if (this.componentsIndex != null && indexSupportsIncludeFilters()) {
return addCandidateComponentsFromIndex(this.componentsIndex, basePackage);
}
else {
return scanCandidateComponents(basePackage);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
Set<BeanDefinition> candidates = new LinkedHashSet<>();
try {
String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
resolveBasePackage(basePackage) + '/' + this.resourcePattern;
Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
boolean traceEnabled = logger.isTraceEnabled();
boolean debugEnabled = logger.isDebugEnabled();
// 拿到包下两个文件的 Resource 实例
for (Resource resource : resources) {
if (traceEnabled) {
logger.trace("Scanning " + resource);
}
if (resource.isReadable()) {
try {
MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
if (isCandidateComponent(metadataReader)) {
ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
sbd.setResource(resource);
sbd.setSource(resource);
// 符合某种条件,暂且就当BF中还没存在这个Bean吧
if (isCandidateComponent(sbd)) {
if (debugEnabled) {
logger.debug("Identified candidate component class: " + resource);
}
// 添加
candidates.add(sbd);
}
else {
if (debugEnabled) {
logger.debug("Ignored because not a concrete top-level class: " + resource);
}
}
}
else {
if (traceEnabled) {
logger.trace("Ignored because not matching any filter: " + resource);
}
}
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to read candidate component class: " + resource, ex);
}
}
else {
if (traceEnabled) {
logger.trace("Ignored because not readable: " + resource);
}
}
}
}
catch (IOException ex) {
throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex);
}
// 返回
return candidates;
}

OK,扫描完成,后在 finishBeanFactoryInitialization 进行初始化就可以了

结束

前言

关于 AOP 在日常开发中应该不算是一个新的名词了,AOP 可以帮助我们把业务之外,但是很多方法都需要调用的方法,无感的方外外部,并且通过配置让外部的 AOP 函数自动织入目标方法中。 常见的业务有:

  1. 记录方法调用的信息,时长;
  2. 初始化当前用户信息上下文,方便在业务内部快速获取。

那我就改改我之前的 Context 用的例子,来康康 Spring 是怎么做的。

准备

applicationContext.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
">

<bean class="cn.liweidan.confbean.MyBeanConfiguration"/>
<context:annotation-config/>
<!-- 开启自动代理支持 -->
<aop:aspectj-autoproxy/>

</beans>

MyBeanConfiguration.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Configurable
public class MyBeanConfiguration {

// @Bean
// public MyBean myBean() {
// MyBean myBean = new MyBean();
// myBean.setName("Weidan");
// return myBean;
// }

@Bean
public NoInterfaceService noInterfaceService() {
return new NoInterfaceService();
}

@Bean
public MethodAop methodAop() {
return new MethodAop();
}

@Bean
public HasInterfaceService hasInterfaceService() {
return new HasInterfaceServiceImpl();
}

}

一个没有接口实现的业务层:

1
2
3
4
5
6
7
public class NoInterfaceService {

public void test() {
System.out.println("这是方法里面的内容");
}

}

一个有接口实现的业务层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface HasInterfaceService {

void testHasInterface();

}

public class HasInterfaceServiceImpl implements HasInterfaceService {

@Override
public void testHasInterface() {
System.out.println("一个有接口的方法的内部");
}

}

一个注解方式的自动织入 Bean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Aspect
public class MethodAop {

@Pointcut("execution(* cn.liweidan.confbean.service.*.*(..)) execution(* cn.liweidan.confbean.services.impl.*.*(..))")
public void aopPoint() {}

@Around("aopPoint()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println(joinPoint.getSignature().getDeclaringTypeName() + "方法开始执行");
Object proceed = joinPoint.proceed();
System.out.println(joinPoint.getSignature().getDeclaringTypeName() + "方法完成执行");
return proceed;
}

}

测试用例:

1
2
3
4
5
6
7
8
9
10
11
12
public class ConfigBeanTest {

@Test
public void testAop() {
BeanFactory bf = new ClassPathXmlApplicationContext("applicationContext.xml");
NoInterfaceService bean = bf.getBean(NoInterfaceService.class);
bean.test();
HasInterfaceService hasInterfaceService = bf.getBean(HasInterfaceService.class);
hasInterfaceService.testHasInterface();
}

}

试运行一下:

1
2
3
4
5
6
cn.liweidan.confbean.service.NoInterfaceService方法开始执行
这是方法里面的内容
cn.liweidan.confbean.service.NoInterfaceService方法完成执行
cn.liweidan.confbean.services.HasInterfaceService方法开始执行
一个有接口的方法的内部
cn.liweidan.confbean.services.HasInterfaceService方法完成执行

好了成功!

注册标签处理器

之前几篇我都直接跳过 xml 解析,这次终于跳不过去了。因为在处理器这一块,Spring 确实能够让我特别惊喜,模块间的解耦仿佛就是我不知道你有什么东西我也不关心但是最后我就能够调用到你的傲娇姑娘的任性。 让我们回到第一篇的 BeanDefinitionDocumentReader读取配置文档 这一节中(传送门),我重新贴一下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
@SuppressWarnings("deprecation")  // for Environment.acceptsProfiles(String...)
protected void doRegisterBeanDefinitions(Element root) {
// Any nested <beans> elements will cause recursion in this method. In
// order to propagate and preserve <beans> default-* attributes correctly,
// keep track of the current (parent) delegate, which may be null. Create
// the new (child) delegate with a reference to the parent for fallback purposes,
// then ultimately reset this.delegate back to its original (parent) reference.
// this behavior emulates a stack of delegates without actually necessitating one.
BeanDefinitionParserDelegate parent = this.delegate;
this.delegate = createDelegate(getReaderContext(), root, parent);

if (this.delegate.isDefaultNamespace(root)) {
String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
if (StringUtils.hasText(profileSpec)) {
String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
// We cannot use Profiles.of(...) since profile expressions are not supported
// in XML config. See SPR-12458 for details.
if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
if (logger.isDebugEnabled()) {
logger.debug("Skipped XML bean definition file due to specified profiles [" + profileSpec +
"] not matching: " + getReaderContext().getResource());
}
return;
}
}
}

preProcessXml(root);
// 来到这句话中,因为这句话会解析到配置文件中的 <aop:aspectj-autoproxy/> 元素中。
parseBeanDefinitions(root, this.delegate);
postProcessXml(root);

this.delegate = parent;
}

// 在这个方法中实现解析:
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
if (delegate.isDefaultNamespace(root)) {
NodeList nl = root.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
if (node instanceof Element) {
Element ele = (Element) node;
if (delegate.isDefaultNamespace(ele)) {
parseDefaultElement(ele, delegate);
}
else {
// 因为 <aop:aspectj-autoproxy/> 并不是默认的命名空间(默认只有:http://www.springframework.org/schema/beans),
// 所以需要调用自定义的处理器来解析元素.
delegate.parseCustomElement(ele);
}
}
}
}
else {
delegate.parseCustomElement(root);
}
}

/**
* Parse a custom element (outside of the default namespace).
* @param ele the element to parse
* @return the resulting bean definition
*/
@Nullable
public BeanDefinition parseCustomElement(Element ele) {
return parseCustomElement(ele, null);
}

/**
* Parse a custom element (outside of the default namespace).
* @param ele the element to parse
* @param containingBd the containing bean definition (if any)
* @return the resulting bean definition
*/
@Nullable
public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) {
// AOP的命名空间是http://www.springframework.org/schema/aop
String namespaceUri = getNamespaceURI(ele);
if (namespaceUri == null) {
return null;
}
// 开始从上下文中寻找合适的解析器,第一个方法只是简单地返回一个实例
// 那下面直接看看 resolve 怎么处理的
NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
if (handler == null) {
error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele);
return null;
}
return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
}

在寻找解析器这一步就挺骚了,直接从所有模块中的 META-INF/spring.handlers 中开始查找其他模块配置的解析器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
@Override
@Nullable
public NamespaceHandler resolve(String namespaceUri) {
// 获取所有的解析器,依赖的所有 jar 包都会读取
Map<String, Object> handlerMappings = getHandlerMappings();
// 因为获取的是字符串,所以需要通过反射实例化解析器对象
Object handlerOrClassName = handlerMappings.get(namespaceUri);
if (handlerOrClassName == null) {
return null;
}
else if (handlerOrClassName instanceof NamespaceHandler) {
return (NamespaceHandler) handlerOrClassName;
}
else {
String className = (String) handlerOrClassName;
try {
Class<?> handlerClass = ClassUtils.forName(className, this.classLoader);
if (!NamespaceHandler.class.isAssignableFrom(handlerClass)) {
throw new FatalBeanException("Class [" + className + "] for namespace [" + namespaceUri +
"] does not implement the [" + NamespaceHandler.class.getName() + "] interface");
}
// 实例化对象
NamespaceHandler namespaceHandler = (NamespaceHandler) BeanUtils.instantiateClass(handlerClass);
namespaceHandler.init();
// 覆盖之前的字符串,以便后面如果还需要可以直接返回对象。
handlerMappings.put(namespaceUri, namespaceHandler);
return namespaceHandler;
}
catch (ClassNotFoundException ex) {
throw new FatalBeanException("Could not find NamespaceHandler class [" + className +
"] for namespace [" + namespaceUri + "]", ex);
}
catch (LinkageError err) {
throw new FatalBeanException("Unresolvable class definition for NamespaceHandler class [" +
className + "] for namespace [" + namespaceUri + "]", err);
}
}
}

/**
* 惰性读取所有解析器.
*/
private Map<String, Object> getHandlerMappings() {
Map<String, Object> handlerMappings = this.handlerMappings;
if (handlerMappings == null) {
synchronized (this) {
handlerMappings = this.handlerMappings;
if (handlerMappings == null) {
if (logger.isTraceEnabled()) {
logger.trace("Loading NamespaceHandler mappings from [" + this.handlerMappingsLocation + "]");
}
try {
// 这一步就是从配置的 handlerMappingsLocation=DEFAULT_HANDLER_MAPPINGS_LOCATION = "META-INF/spring.handlers" 中去查找其他模块编写的配置文件,然后获取到对象的自定义标签的解析器
Properties mappings =
PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);
if (logger.isTraceEnabled()) {
logger.trace("Loaded NamespaceHandler mappings: " + mappings);
}
// 开始加载解析器,然后缓存起来返回到上面的方法中。
handlerMappings = new ConcurrentHashMap<>(mappings.size());
CollectionUtils.mergePropertiesIntoMap(mappings, handlerMappings);
this.handlerMappings = handlerMappings;
}
catch (IOException ex) {
throw new IllegalStateException(
"Unable to load NamespaceHandler mappings from location [" + this.handlerMappingsLocation + "]", ex);
}
}
}
}
return handlerMappings;
}

AOP模块中的 META-INF/spring.handlers

1
http\://www.springframework.org/schema/aop=org.springframework.aop.config.AopNamespaceHandler

然后就可以开始使用其他模块的处理器开始处理元素了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Nullable
public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) {
// AOP的命名空间是http://www.springframework.org/schema/aop
String namespaceUri = getNamespaceURI(ele);
if (namespaceUri == null) {
return null;
}
// 开始从上下文中寻找合适的解析器,第一个方法只是简单地返回一个实例
// 那下面直接看看 resolve 怎么处理的
NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
if (handler == null) {
error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele);
return null;
}
// 开始处理
return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
}

初始化AOP所需环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// NamespaceHandler内的方法:
@Override
@Nullable
public BeanDefinition parse(Element element, ParserContext parserContext) {
BeanDefinitionParser parser = findParserForElement(element, parserContext);
return (parser != null ? parser.parse(element, parserContext) : null);
}
// 还有一个抽象的方法
@Nullable
BeanDefinition parse(Element element, ParserContext parserContext);

//AopNamespaceHandler处理器的实现:
@Override
@Nullable
public BeanDefinition parse(Element element, ParserContext parserContext) {
AopNamespaceUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(parserContext, element);
extendBeanDefinition(element, parserContext);
return null;
}

首先看看 AopNamespaceUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(parserContext, element); 这句话,看方法名初步判断是注册一个 BeanDefinition 以便后续用来解析基于 Java注解AOP Advisor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public static void registerAspectJAnnotationAutoProxyCreatorIfNecessary(
ParserContext parserContext, Element sourceElement) {

BeanDefinition beanDefinition = AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(
parserContext.getRegistry(), parserContext.extractSource(sourceElement));
// 因为只是简单配置标签,没有定义属性,所以这个方法里面是个空的执行
useClassProxyingIfNecessary(parserContext.getRegistry(), sourceElement);
// 将第一句话拿到的 beanDefinition 注册到 bf 中去,使用 Component 组的形式进行注册。
registerComponentIfNecessary(beanDefinition, parserContext);
}
// 调用了这里:
@Nullable
public static BeanDefinition registerAspectJAnnotationAutoProxyCreatorIfNecessary(
BeanDefinitionRegistry registry, @Nullable Object source) {

return registerOrEscalateApcAsRequired(AnnotationAwareAspectJAutoProxyCreator.class, registry, source);
}
// 然后再继续调用了这里:
@Nullable
private static BeanDefinition registerOrEscalateApcAsRequired(
Class<?> cls, BeanDefinitionRegistry registry, @Nullable Object source) {

Assert.notNull(registry, "BeanDefinitionRegistry must not be null");

if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) {
BeanDefinition apcDefinition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME);
if (!cls.getName().equals(apcDefinition.getBeanClassName())) {
int currentPriority = findPriorityForClass(apcDefinition.getBeanClassName());
int requiredPriority = findPriorityForClass(cls);
if (currentPriority < requiredPriority) {
apcDefinition.setBeanClassName(cls.getName());
}
}
return null;
}

RootBeanDefinition beanDefinition = new RootBeanDefinition(cls);
beanDefinition.setSource(source);
beanDefinition.getPropertyValues().add("order", Ordered.HIGHEST_PRECEDENCE);
beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
registry.registerBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME, beanDefinition);
return beanDefinition;
}

果然就是注册了一个 AnnotationAwareAspectJAutoProxyCreator 这个东西,用来创建代理的玩意儿。 先看看这个类的继承体系:

前言

关于 AOP 在日常开发中应该不算是一个新的名词了,AOP 可以帮助我们把业务之外,但是很多方法都需要调用的方法,无感的方外外部,并且通过配置让外部的 AOP 函数自动织入目标方法中。 常见的业务有:

  1. 记录方法调用的信息,时长;
  2. 初始化当前用户信息上下文,方便在业务内部快速获取。

那我就改改我之前的 Context 用的例子,来康康 Spring 是怎么做的。

准备

applicationContext.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
">

<bean class="cn.liweidan.confbean.MyBeanConfiguration"/>
<context:annotation-config/>
<!-- 开启自动代理支持 -->
<aop:aspectj-autoproxy/>

</beans>

MyBeanConfiguration.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Configurable
public class MyBeanConfiguration {

// @Bean
// public MyBean myBean() {
// MyBean myBean = new MyBean();
// myBean.setName("Weidan");
// return myBean;
// }

@Bean
public NoInterfaceService noInterfaceService() {
return new NoInterfaceService();
}

@Bean
public MethodAop methodAop() {
return new MethodAop();
}

@Bean
public HasInterfaceService hasInterfaceService() {
return new HasInterfaceServiceImpl();
}

}

一个没有接口实现的业务层:

1
2
3
4
5
6
7
public class NoInterfaceService {

public void test() {
System.out.println("这是方法里面的内容");
}

}

一个有接口实现的业务层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface HasInterfaceService {

void testHasInterface();

}

public class HasInterfaceServiceImpl implements HasInterfaceService {

@Override
public void testHasInterface() {
System.out.println("一个有接口的方法的内部");
}

}

一个注解方式的自动织入 Bean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Aspect
public class MethodAop {

@Pointcut("execution(* cn.liweidan.confbean.service.*.*(..)) execution(* cn.liweidan.confbean.services.impl.*.*(..))")
public void aopPoint() {}

@Around("aopPoint()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println(joinPoint.getSignature().getDeclaringTypeName() + "方法开始执行");
Object proceed = joinPoint.proceed();
System.out.println(joinPoint.getSignature().getDeclaringTypeName() + "方法完成执行");
return proceed;
}

}

测试用例:

1
2
3
4
5
6
7
8
9
10
11
12
public class ConfigBeanTest {

@Test
public void testAop() {
BeanFactory bf = new ClassPathXmlApplicationContext("applicationContext.xml");
NoInterfaceService bean = bf.getBean(NoInterfaceService.class);
bean.test();
HasInterfaceService hasInterfaceService = bf.getBean(HasInterfaceService.class);
hasInterfaceService.testHasInterface();
}

}

试运行一下:

1
2
3
4
5
6
cn.liweidan.confbean.service.NoInterfaceService方法开始执行
这是方法里面的内容
cn.liweidan.confbean.service.NoInterfaceService方法完成执行
cn.liweidan.confbean.services.HasInterfaceService方法开始执行
一个有接口的方法的内部
cn.liweidan.confbean.services.HasInterfaceService方法完成执行

好了成功!

注册标签处理器

之前几篇我都直接跳过 xml 解析,这次终于跳不过去了。因为在处理器这一块,Spring 确实能够让我特别惊喜,模块间的解耦仿佛就是我不知道你有什么东西我也不关心但是最后我就能够调用到你的傲娇姑娘的任性。 让我们回到第一篇的 BeanDefinitionDocumentReader读取配置文档 这一节中(传送门),我重新贴一下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
@SuppressWarnings("deprecation")  // for Environment.acceptsProfiles(String...)
protected void doRegisterBeanDefinitions(Element root) {
// Any nested <beans> elements will cause recursion in this method. In
// order to propagate and preserve <beans> default-* attributes correctly,
// keep track of the current (parent) delegate, which may be null. Create
// the new (child) delegate with a reference to the parent for fallback purposes,
// then ultimately reset this.delegate back to its original (parent) reference.
// this behavior emulates a stack of delegates without actually necessitating one.
BeanDefinitionParserDelegate parent = this.delegate;
this.delegate = createDelegate(getReaderContext(), root, parent);

if (this.delegate.isDefaultNamespace(root)) {
String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
if (StringUtils.hasText(profileSpec)) {
String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
// We cannot use Profiles.of(...) since profile expressions are not supported
// in XML config. See SPR-12458 for details.
if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
if (logger.isDebugEnabled()) {
logger.debug("Skipped XML bean definition file due to specified profiles [" + profileSpec +
"] not matching: " + getReaderContext().getResource());
}
return;
}
}
}

preProcessXml(root);
// 来到这句话中,因为这句话会解析到配置文件中的 <aop:aspectj-autoproxy/> 元素中。
parseBeanDefinitions(root, this.delegate);
postProcessXml(root);

this.delegate = parent;
}

// 在这个方法中实现解析:
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
if (delegate.isDefaultNamespace(root)) {
NodeList nl = root.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
if (node instanceof Element) {
Element ele = (Element) node;
if (delegate.isDefaultNamespace(ele)) {
parseDefaultElement(ele, delegate);
}
else {
// 因为 <aop:aspectj-autoproxy/> 并不是默认的命名空间(默认只有:http://www.springframework.org/schema/beans),
// 所以需要调用自定义的处理器来解析元素.
delegate.parseCustomElement(ele);
}
}
}
}
else {
delegate.parseCustomElement(root);
}
}

/**
* Parse a custom element (outside of the default namespace).
* @param ele the element to parse
* @return the resulting bean definition
*/
@Nullable
public BeanDefinition parseCustomElement(Element ele) {
return parseCustomElement(ele, null);
}

/**
* Parse a custom element (outside of the default namespace).
* @param ele the element to parse
* @param containingBd the containing bean definition (if any)
* @return the resulting bean definition
*/
@Nullable
public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) {
// AOP的命名空间是http://www.springframework.org/schema/aop
String namespaceUri = getNamespaceURI(ele);
if (namespaceUri == null) {
return null;
}
// 开始从上下文中寻找合适的解析器,第一个方法只是简单地返回一个实例
// 那下面直接看看 resolve 怎么处理的
NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
if (handler == null) {
error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele);
return null;
}
return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
}

在寻找解析器这一步就挺骚了,直接从所有模块中的 META-INF/spring.handlers 中开始查找其他模块配置的解析器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
@Override
@Nullable
public NamespaceHandler resolve(String namespaceUri) {
// 获取所有的解析器,依赖的所有 jar 包都会读取
Map<String, Object> handlerMappings = getHandlerMappings();
// 因为获取的是字符串,所以需要通过反射实例化解析器对象
Object handlerOrClassName = handlerMappings.get(namespaceUri);
if (handlerOrClassName == null) {
return null;
}
else if (handlerOrClassName instanceof NamespaceHandler) {
return (NamespaceHandler) handlerOrClassName;
}
else {
String className = (String) handlerOrClassName;
try {
Class<?> handlerClass = ClassUtils.forName(className, this.classLoader);
if (!NamespaceHandler.class.isAssignableFrom(handlerClass)) {
throw new FatalBeanException("Class [" + className + "] for namespace [" + namespaceUri +
"] does not implement the [" + NamespaceHandler.class.getName() + "] interface");
}
// 实例化对象
NamespaceHandler namespaceHandler = (NamespaceHandler) BeanUtils.instantiateClass(handlerClass);
namespaceHandler.init();
// 覆盖之前的字符串,以便后面如果还需要可以直接返回对象。
handlerMappings.put(namespaceUri, namespaceHandler);
return namespaceHandler;
}
catch (ClassNotFoundException ex) {
throw new FatalBeanException("Could not find NamespaceHandler class [" + className +
"] for namespace [" + namespaceUri + "]", ex);
}
catch (LinkageError err) {
throw new FatalBeanException("Unresolvable class definition for NamespaceHandler class [" +
className + "] for namespace [" + namespaceUri + "]", err);
}
}
}

/**
* 惰性读取所有解析器.
*/
private Map<String, Object> getHandlerMappings() {
Map<String, Object> handlerMappings = this.handlerMappings;
if (handlerMappings == null) {
synchronized (this) {
handlerMappings = this.handlerMappings;
if (handlerMappings == null) {
if (logger.isTraceEnabled()) {
logger.trace("Loading NamespaceHandler mappings from [" + this.handlerMappingsLocation + "]");
}
try {
// 这一步就是从配置的 handlerMappingsLocation=DEFAULT_HANDLER_MAPPINGS_LOCATION = "META-INF/spring.handlers" 中去查找其他模块编写的配置文件,然后获取到对象的自定义标签的解析器
Properties mappings =
PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);
if (logger.isTraceEnabled()) {
logger.trace("Loaded NamespaceHandler mappings: " + mappings);
}
// 开始加载解析器,然后缓存起来返回到上面的方法中。
handlerMappings = new ConcurrentHashMap<>(mappings.size());
CollectionUtils.mergePropertiesIntoMap(mappings, handlerMappings);
this.handlerMappings = handlerMappings;
}
catch (IOException ex) {
throw new IllegalStateException(
"Unable to load NamespaceHandler mappings from location [" + this.handlerMappingsLocation + "]", ex);
}
}
}
}
return handlerMappings;
}

AOP模块中的 META-INF/spring.handlers

1
http\://www.springframework.org/schema/aop=org.springframework.aop.config.AopNamespaceHandler

然后就可以开始使用其他模块的处理器开始处理元素了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Nullable
public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) {
// AOP的命名空间是http://www.springframework.org/schema/aop
String namespaceUri = getNamespaceURI(ele);
if (namespaceUri == null) {
return null;
}
// 开始从上下文中寻找合适的解析器,第一个方法只是简单地返回一个实例
// 那下面直接看看 resolve 怎么处理的
NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
if (handler == null) {
error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele);
return null;
}
// 开始处理
return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
}

初始化AOP所需环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// NamespaceHandler内的方法:
@Override
@Nullable
public BeanDefinition parse(Element element, ParserContext parserContext) {
BeanDefinitionParser parser = findParserForElement(element, parserContext);
return (parser != null ? parser.parse(element, parserContext) : null);
}
// 还有一个抽象的方法
@Nullable
BeanDefinition parse(Element element, ParserContext parserContext);

//AopNamespaceHandler处理器的实现:
@Override
@Nullable
public BeanDefinition parse(Element element, ParserContext parserContext) {
AopNamespaceUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(parserContext, element);
extendBeanDefinition(element, parserContext);
return null;
}

首先看看 AopNamespaceUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(parserContext, element); 这句话,看方法名初步判断是注册一个 BeanDefinition 以便后续用来解析基于 Java注解AOP Advisor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public static void registerAspectJAnnotationAutoProxyCreatorIfNecessary(
ParserContext parserContext, Element sourceElement) {

BeanDefinition beanDefinition = AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(
parserContext.getRegistry(), parserContext.extractSource(sourceElement));
// 因为只是简单配置标签,没有定义属性,所以这个方法里面是个空的执行
useClassProxyingIfNecessary(parserContext.getRegistry(), sourceElement);
// 将第一句话拿到的 beanDefinition 注册到 bf 中去,使用 Component 组的形式进行注册。
registerComponentIfNecessary(beanDefinition, parserContext);
}
// 调用了这里:
@Nullable
public static BeanDefinition registerAspectJAnnotationAutoProxyCreatorIfNecessary(
BeanDefinitionRegistry registry, @Nullable Object source) {

return registerOrEscalateApcAsRequired(AnnotationAwareAspectJAutoProxyCreator.class, registry, source);
}
// 然后再继续调用了这里:
@Nullable
private static BeanDefinition registerOrEscalateApcAsRequired(
Class<?> cls, BeanDefinitionRegistry registry, @Nullable Object source) {

Assert.notNull(registry, "BeanDefinitionRegistry must not be null");

if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) {
BeanDefinition apcDefinition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME);
if (!cls.getName().equals(apcDefinition.getBeanClassName())) {
int currentPriority = findPriorityForClass(apcDefinition.getBeanClassName());
int requiredPriority = findPriorityForClass(cls);
if (currentPriority < requiredPriority) {
apcDefinition.setBeanClassName(cls.getName());
}
}
return null;
}

RootBeanDefinition beanDefinition = new RootBeanDefinition(cls);
beanDefinition.setSource(source);
beanDefinition.getPropertyValues().add("order", Ordered.HIGHEST_PRECEDENCE);
beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
registry.registerBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME, beanDefinition);
return beanDefinition;
}

果然就是注册了一个 AnnotationAwareAspectJAutoProxyCreator 这个东西,用来创建代理的玩意儿。 先看看这个类的继承体系:

稍微看看左上角的 BeanPostProcessor,OK,是初始化容器的时候通过他来注入 AOP 内容。

解析Advisor配置的Bean

好了,那么环境初始化好了,MethodAop 这个 Advisor 也注册进去了,接下来就是运行的时候织入 Bean 的时候了。 根据调试查看,配置中的 Bean 是在 refresh() 方法中的 finishBeanFactoryInitialization(beanFactory); 这句话来织入并且生成代理对象的。 至于怎么创建对象的,之前有说过,现在要说的只不过是创建对象以后,使用 BeanPostProcessor 把建好的对象给替换掉。 所以直接点,来到 AbstractAutowireCapableBeanFactory#doCreateBean() 看看怎么操作的。(所以无论是不是配置中的 Bean,只要使用该方法来创建的,就是该 Bean 被织入的时机)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
throws BeanCreationException {

// Instantiate the bean.
BeanWrapper instanceWrapper = null;
if (mbd.isSingleton()) {
instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
}
if (instanceWrapper == null) {
instanceWrapper = createBeanInstance(beanName, mbd, args);
}
final Object bean = instanceWrapper.getWrappedInstance();
Class<?> beanType = instanceWrapper.getWrappedClass();
if (beanType != NullBean.class) {
mbd.resolvedTargetType = beanType;
}

// Allow post-processors to modify the merged bean definition.
synchronized (mbd.postProcessingLock) {
if (!mbd.postProcessed) {
try {
applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName);
}
catch (Throwable ex) {
throw new BeanCreationException(mbd.getResourceDescription(), beanName,
"Post-processing of merged bean definition failed", ex);
}
mbd.postProcessed = true;
}
}

// Eagerly cache singletons to be able to resolve circular references
// even when triggered by lifecycle interfaces like BeanFactoryAware.
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
if (logger.isTraceEnabled()) {
logger.trace("Eagerly caching bean '" + beanName +
"' to allow for resolving potential circular references");
}
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}

// Initialize the bean instance.
Object exposedObject = bean;
try {
populateBean(beanName, mbd, instanceWrapper);
// 初始化对象,这句话会调用BeanPostProcessor的before和After的方法
// 织入的时机主要是在 postProcessAfterInitialization 方法中.
exposedObject = initializeBean(beanName, exposedObject, mbd);
}
catch (Throwable ex) {
if (ex instanceof BeanCreationException && beanName.equals(((BeanCreationException) ex).getBeanName())) {
throw (BeanCreationException) ex;
}
else {
throw new BeanCreationException(
mbd.getResourceDescription(), beanName, "Initialization of bean failed", ex);
}
}

if (earlySingletonExposure) {
Object earlySingletonReference = getSingleton(beanName, false);
if (earlySingletonReference != null) {
if (exposedObject == bean) {
exposedObject = earlySingletonReference;
}
else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
String[] dependentBeans = getDependentBeans(beanName);
Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);
for (String dependentBean : dependentBeans) {
if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
actualDependentBeans.add(dependentBean);
}
}
if (!actualDependentBeans.isEmpty()) {
throw new BeanCurrentlyInCreationException(beanName,
"Bean with name '" + beanName + "' has been injected into other beans [" +
StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +
"] in its raw version as part of a circular reference, but has eventually been " +
"wrapped. This means that said other beans do not use the final version of the " +
"bean. This is often the result of over-eager type matching - consider using " +
"'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.");
}
}
}
}

// Register bean as disposable.
try {
registerDisposableBeanIfNecessary(beanName, bean, mbd);
}
catch (BeanDefinitionValidationException ex) {
throw new BeanCreationException(
mbd.getResourceDescription(), beanName, "Invalid destruction signature", ex);
}

return exposedObject;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
class AbstractAutowireCapableBeanFactory

protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) {
if (System.getSecurityManager() != null) {
AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
invokeAwareMethods(beanName, bean);
return null;
}, getAccessControlContext());
}
else {
invokeAwareMethods(beanName, bean);
}

Object wrappedBean = bean;
if (mbd == null !mbd.isSynthetic()) {
wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
}

try {
invokeInitMethods(beanName, wrappedBean, mbd);
}
catch (Throwable ex) {
throw new BeanCreationException(
(mbd != null ? mbd.getResourceDescription() : null),
beanName, "Invocation of init method failed", ex);
}
if (mbd == null !mbd.isSynthetic()) {
// 应用Bean创建的后处理器,包装Bean
wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
}

return wrappedBean;
}

// 每个Bean创建就一顿循环 BeanPostProcessors,调用 BeanPostProcessor#postProcessAfterInitialization来处理实例
// 那么刚刚上面我们说过,环境初始化的时候,Spring偷偷放进去了一个 AnnotationAwareAspectJAutoProxyCreator
// 他其实就是一个BeanPostProcessors,所以不用说肯定在这个类里边处理
@Override
public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName)
throws BeansException {

Object result = existingBean;
for (BeanPostProcessor processor : getBeanPostProcessors()) {
Object current = processor.postProcessAfterInitialization(result, beanName);
if (current == null) {
return result;
}
result = current;
}
return result;
}

分享个 debug 小技巧,因为是个循环,然后又很难看出来他循环到哪个类,稍微手抖了一下,又跳过了这个类又要重新开始 debug。所以我们可以使用 idea 提供的条件断点,我这里是使用 class 来比较,如果是我想要的这个 class 那么就停止在这个断点:

接下来进入看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* Create a proxy with the configured interceptors if the bean is
* identified as one to proxy by the subclass.
* @see #getAdvicesAndAdvisorsForBean
*/
@Override
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
if (bean != null) {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
// 提早曝光的代理引用,如果不是相同的,则调用 wrapIfNecessary 进行包装
if (this.earlyProxyReferences.remove(cacheKey) != bean) {
return wrapIfNecessary(bean, beanName, cacheKey);
}
}
return bean;
}
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
return bean;
}
// 这里通过 cacheKey 判断当前 Bean 是否之前记录不需要切入(使用 advisedBeans 存储)
if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
return bean;
}
// 1. 如果是 Advice/Pointcut 等 AOP 相关的类则跳过;
// 2. BeanName以.ORIGINAL结尾的话,不做代理,如:com.mypackage.MyClass.ORIGINAL
if (isInfrastructureClass(bean.getClass()) shouldSkip(bean.getClass(), beanName)) {
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}

// 在这里开始获取 Advisor
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
if (specificInterceptors != DO_NOT_PROXY) {
this.advisedBeans.put(cacheKey, Boolean.TRUE);
// 开始创建代理对象
Object proxy = createProxy(
bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
this.proxyTypes.put(cacheKey, proxy.getClass());
return proxy;
}

this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}

获取advisor

如果暂时对如何获取 advisor 没兴趣的话,可以先跳过,后面再回来看

1
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);

这句话是用来获取 advisor 的,并且缓存起来项目中配置的 advisor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Override
@Nullable
protected Object[] getAdvicesAndAdvisorsForBean(
Class<?> beanClass, String beanName, @Nullable TargetSource targetSource) {

List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName);
if (advisors.isEmpty()) {
return DO_NOT_PROXY;
}
return advisors.toArray();
}

protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
List<Advisor> candidateAdvisors = findCandidateAdvisors();
List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
extendAdvisors(eligibleAdvisors);
if (!eligibleAdvisors.isEmpty()) {
eligibleAdvisors = sortAdvisors(eligibleAdvisors);
}
return eligibleAdvisors;
}

List<Advisor> candidateAdvisors = findCandidateAdvisors(); 这句话使用来获取已经缓存起来了的所有 Advisors。因为在当前我们已经无法进入到怎么解析并缓存的问题上来了(因为已经缓存起来了),所以我们需要退回到前面的步骤中来。

解析Advisor

然而什么时候做的解析和缓存,我们刚刚在上一节中说了 initializeBean 会在创建所有 Bean 的时候调用所有 beanPostProcessor 的前置处理器。那我们来看看 AnnotationAwareAspectJAutoProxyCreator 的前置处理器做了什么,并且第一次调用该方法是什么时候:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
@Override
public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) {
Object cacheKey = getCacheKey(beanClass, beanName);

// 这里在前置的时候就开始记录哪些 Bean 需要织入,哪些不需要了
if (!StringUtils.hasLength(beanName) !this.targetSourcedBeans.contains(beanName)) {
if (this.advisedBeans.containsKey(cacheKey)) {
return null;
}
// 初始化第一个 Bean(在我这里是我那个配置类的初始化)的时候就开始调用 shouldSkip
if (isInfrastructureClass(beanClass) shouldSkip(beanClass, beanName)) {
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return null;
}
}

// Create proxy here if we have a custom TargetSource.
// Suppresses unnecessary default instantiation of the target bean:
// The TargetSource will handle target instances in a custom fashion.
TargetSource targetSource = getCustomTargetSource(beanClass, beanName);
if (targetSource != null) {
if (StringUtils.hasLength(beanName)) {
this.targetSourcedBeans.add(beanName);
}
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(beanClass, beanName, targetSource);
Object proxy = createProxy(beanClass, beanName, specificInterceptors, targetSource);
this.proxyTypes.put(cacheKey, proxy.getClass());
return proxy;
}

return null;
}
@Override
protected boolean shouldSkip(Class<?> beanClass, String beanName) {
// 获取当前 BeanFactory 中所有的 advisor 对象,并且做缓存
List<Advisor> candidateAdvisors = findCandidateAdvisors();
for (Advisor advisor : candidateAdvisors) {
if (advisor instanceof AspectJPointcutAdvisor &&
((AspectJPointcutAdvisor) advisor).getAspectName().equals(beanName)) {
return true;
}
}
return super.shouldSkip(beanClass, beanName);
}

protected List<Advisor> findCandidateAdvisors() {
// 先调用下面那个在父类的方法
List<Advisor> advisors = super.findCandidateAdvisors();
// 开始构建存在 BeanFactory 中的 Advisor
if (this.aspectJAdvisorsBuilder != null) {
advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors());
}
return advisors;
}
protected List<Advisor> findCandidateAdvisors() {
Assert.state(this.advisorRetrievalHelper != null, "No BeanFactoryAdvisorRetrievalHelper available");
// 通过一个 advisorRetrievalHelper 来获取所有的 advisor Bean
return this.advisorRetrievalHelper.findAdvisorBeans();
}

advisorRetrievalHelper 是配置 auto-proxy 的时候,会被使用的一个类,他在 AnnotationAwareAspectJAutoProxyCreator 中传入一个 ConfigurableListableBeanFactory 作为参数调用构造器初始化。 所以,advisorRetrievalHelper 可以在当前容器中,查找所有的 BeanDefinition,并且取出是 AdvisorBean。 接下来来看看这个方法做了什么事情:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* Find all eligible Advisor beans in the current bean factory,
* ignoring FactoryBeans and excluding beans that are currently in creation.
* @return the list of {@link org.springframework.aop.Advisor} beans
* @see #isEligibleBean
*/
public List<Advisor> findAdvisorBeans() {
// Determine list of advisor bean names, if not cached already.
String[] advisorNames = this.cachedAdvisorBeanNames;
if (advisorNames == null) {
// Do not initialize FactoryBeans here: We need to leave all regular beans
// uninitialized to let the auto-proxy creator apply to them!
advisorNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
this.beanFactory, Advisor.class, true, false);
this.cachedAdvisorBeanNames = advisorNames;
}
// 第一次调用的时候,还没找到,所以直接就在这里被返回出去
if (advisorNames.length == 0) {
return new ArrayList<>();
}

List<Advisor> advisors = new ArrayList<>();
for (String name : advisorNames) {
if (isEligibleBean(name)) {
if (this.beanFactory.isCurrentlyInCreation(name)) {
if (logger.isTraceEnabled()) {
logger.trace("Skipping currently created advisor '" + name + "'");
}
}
else {
try {
advisors.add(this.beanFactory.getBean(name, Advisor.class));
}
catch (BeanCreationException ex) {
.....
continue;
}
}
throw ex;
}
}
}
}
return advisors;
}

OK,既然返回了一个空的集合,那么就来看看,后面做了什么事情了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
protected List<Advisor> findCandidateAdvisors() {
// 先调用下面那个在父类的方法
List<Advisor> advisors = super.findCandidateAdvisors();
// 开始构建存在 BeanFactory 中的 Advisor
if (this.aspectJAdvisorsBuilder != null) {
advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors());
}
return advisors;
}
public List<Advisor> buildAspectJAdvisors() {
List<String> aspectNames = this.aspectBeanNames;

if (aspectNames == null) {
synchronized (this) {
// 因为 aspectBeanNames 现在是 NULL,所以进入开始初始化
aspectNames = this.aspectBeanNames;
if (aspectNames == null) {
List<Advisor> advisors = new ArrayList<>();
aspectNames = new ArrayList<>();
// 获取 BeanFactory 中所有的 beanNames
String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
this.beanFactory, Object.class, true, false);
// 二话不说就来了个循环
for (String beanName : beanNames) {
// 是否是个有资格的类?然而方法都是直接返回 true...也就是这一步永远不会进去
// 感觉这个方法是个有故事的方法,先不看
if (!isEligibleBean(beanName)) {
continue;
}
// 这一步必须使用 getType 调用 getBean 可能会导致错误(那个Bean需要被织入但是太早初始化的
// 话会导致没有织入成功)
Class<?> beanType = this.beanFactory.getType(beanName);
if (beanType == null) {
continue;
}
// 这一步会判断类有没有被 @Aspect 修饰
if (this.advisorFactory.isAspect(beanType)) {
aspectNames.add(beanName);
AspectMetadata amd = new AspectMetadata(beanType, beanName);
// 根据 Advisor 是否可以为单例(这个是AOP定义的单例)来缓存 AOP 信息以便后面可以使用
if (amd.getAjType().getPerClause().getKind() == PerClauseKind.SINGLETON) {
// 构建 Advisor 工厂
MetadataAwareAspectInstanceFactory factory =
new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName);
List<Advisor> classAdvisors = this.advisorFactory.getAdvisors(factory);
// 如果是单例(BeanFactory的单例)的 Bean,则直接缓存 Advisor 对象
if (this.beanFactory.isSingleton(beanName)) {
this.advisorsCache.put(beanName, classAdvisors);
}
else {
this.aspectFactoryCache.put(beanName, factory);
}
advisors.addAll(classAdvisors);
}
else {
// 每个目标对象或者每个对象都去织入.
if (this.beanFactory.isSingleton(beanName)) {
throw new IllegalArgumentException("Bean with name '" + beanName +
"' is a singleton, but aspect instantiation model is not singleton");
}
MetadataAwareAspectInstanceFactory factory =
new PrototypeAspectInstanceFactory(this.beanFactory, beanName);
this.aspectFactoryCache.put(beanName, factory);
advisors.addAll(this.advisorFactory.getAdvisors(factory));
}
}
}
this.aspectBeanNames = aspectNames;
return advisors;
}
}
}

// 下一次再调用该方法的时候,即可直接通过缓存中取出 Advisor 使用
if (aspectNames.isEmpty()) {
return Collections.emptyList();
}
List<Advisor> advisors = new ArrayList<>();
for (String aspectName : aspectNames) {
List<Advisor> cachedAdvisors = this.advisorsCache.get(aspectName);
if (cachedAdvisors != null) {
advisors.addAll(cachedAdvisors);
}
else {
MetadataAwareAspectInstanceFactory factory = this.aspectFactoryCache.get(aspectName);
advisors.addAll(this.advisorFactory.getAdvisors(factory));
}
}
return advisors;
}

获取所有的Advisor

刚刚我们来到这里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
@Nullable
protected Object[] getAdvicesAndAdvisorsForBean(
Class<?> beanClass, String beanName, @Nullable TargetSource targetSource) {

List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName);
if (advisors.isEmpty()) {
return DO_NOT_PROXY;
}
return advisors.toArray();
}

protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
List<Advisor> candidateAdvisors = findCandidateAdvisors();// <-----
// 获取装配当前 Bean 的 Advisor
// 这一步其实就是获取 @Pointcut 中配置的装配规则来匹配所有的方法,我偷懒,先过
List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
// 扩展Advisor
extendAdvisors(eligibleAdvisors);
if (!eligibleAdvisors.isEmpty()) {
eligibleAdvisors = sortAdvisors(eligibleAdvisors);
}
return eligibleAdvisors;
}

现在在这个方法内部,已经获取我们自己配置的 MethodAop 对象了,但是下面还有个扩展,看看扩展了什么东西:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* Adds an {@link ExposeInvocationInterceptor} to the beginning of the advice chain.
* These additional advices are needed when using AspectJ expression pointcuts
* and when using AspectJ-style advice.
*/
@Override
protected void extendAdvisors(List<Advisor> candidateAdvisors) {
AspectJProxyUtils.makeAdvisorChainAspectJCapableIfNecessary(candidateAdvisors);
}

public static boolean makeAdvisorChainAspectJCapableIfNecessary(List<Advisor> advisors) {
// Don't add advisors to an empty list; may indicate that proxying is just not required
if (!advisors.isEmpty()) {
boolean foundAspectJAdvice = false;
for (Advisor advisor : advisors) {
// 如果是 AspectJAdvice 则不添加
if (isAspectJAdvice(advisor)) {
foundAspectJAdvice = true;
break;
}
}
// 我们织入的方法中,都会添加一个 ExposeInvocationInterceptor 实例
if (foundAspectJAdvice && !advisors.contains(ExposeInvocationInterceptor.ADVISOR)) {
advisors.add(0, ExposeInvocationInterceptor.ADVISOR);
return true;
}
}
return false;
}

这里有两个概念:

  1. AspectJAdvice:通过编译器织入 AOP 代码的一个框架;

  2. ExposeInvocationInterceptor:这是一个运行期织入时可以随时获取整个调用链的内置的 AOP 拦截器,如果在使用过程中需要获取调用链的话,可以通过 ExposeInvocationInterceptor.currentInvocation 来获取。SpringAOP 在织入的时候默认在第一个织入这个实例。

好了,走完全部,获取所有 advisors 的过程终于完成。

织入运行期对象

回到 postProcessAfterInitialization -> wrapIfNecessary 方法中来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
return bean;
}
// 这里通过 cacheKey 判断当前 Bean 是否之前记录不需要切入(使用 advisedBeans 存储)
if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
return bean;
}
// 1. 如果是 Advice/Pointcut 等 AOP 相关的类则跳过;
// 2. BeanName以.ORIGINAL结尾的话,不做代理,如:com.mypackage.MyClass.ORIGINAL
if (isInfrastructureClass(bean.getClass()) shouldSkip(bean.getClass(), beanName)) {
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}

// 在这里开始获取 Advisor
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
if (specificInterceptors != DO_NOT_PROXY) {
this.advisedBeans.put(cacheKey, Boolean.TRUE);
// 开始创建代理对象
Object proxy = createProxy(
bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
this.proxyTypes.put(cacheKey, proxy.getClass());
return proxy;
}

this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}

接下来就是开始创建代理对象了。创建对象有两种方式:cglibjdk代理,使用哪种方式取决于当前那个类有没有接口实现,如果是一个普通的类(没有实现接口的方式),则使用前者,否则用后者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
@Nullable Object[] specificInterceptors, TargetSource targetSource) {

if (this.beanFactory instanceof ConfigurableListableBeanFactory) {
// 记录原始数据
AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass);
}

// 创建 ProxyFactory 用于创建代理
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.copyFrom(this);

if (!proxyFactory.isProxyTargetClass()) {
if (shouldProxyTargetClass(beanClass, beanName)) {
// 直接代理类实例(通过BeanDefinition配置的属性来判断)
proxyFactory.setProxyTargetClass(true);
}
else {
// 计算所有的接口设置到工厂,如果没有接口,同上直接代理实例
evaluateProxyInterfaces(beanClass, proxyFactory);
}
}

// 通过上一步获取的所有 advisors,过滤获取匹配的 advisors
Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
proxyFactory.addAdvisors(advisors);
proxyFactory.setTargetSource(targetSource);
customizeProxyFactory(proxyFactory);

proxyFactory.setFrozen(this.freezeProxy);
if (advisorsPreFiltered()) {
proxyFactory.setPreFiltered(true);
}

// 开始构建
return proxyFactory.getProxy(getProxyClassLoader());
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public Object getProxy(@Nullable ClassLoader classLoader) {
// 调用 DefaultAopProxyFactory 开始构建代理
return createAopProxy().getProxy(classLoader);
}
protected final synchronized AopProxy createAopProxy() {
if (!this.active) {
activate();
}
// getAopProxyFactory返回DefaultAopProxyFactory实例
return getAopProxyFactory().createAopProxy(this);
}
DefaultAopProxyFactory#createAopProxy
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
if (config.isOptimize() config.isProxyTargetClass() hasNoUserSuppliedProxyInterfaces(config)) {
Class<?> targetClass = config.getTargetClass();
if (targetClass == null) {
throw new AopConfigException("TargetSource cannot determine target class: " +
"Either an interface or a target is required for proxy creation.");
}
if (targetClass.isInterface() Proxy.isProxyClass(targetClass)) {
return new JdkDynamicAopProxy(config);
}
// 如果没有接口,来到这
return new ObjenesisCglibAopProxy(config);
}
else {
return new JdkDynamicAopProxy(config);
}
}

createAopProxy 之后就是调用各自的实现 getProxy(classLoader)

Cglib

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
@Override
public Object getProxy(@Nullable ClassLoader classLoader) {
if (logger.isTraceEnabled()) {
logger.trace("Creating CGLIB proxy: " + this.advised.getTargetSource());
}

try {
// 获取对象的实际类型
Class<?> rootClass = this.advised.getTargetClass();
Assert.state(rootClass != null, "Target class must be available for creating a CGLIB proxy");

// 如果已经是一个 cglib 对象的时候,获取其父级(实际类型)
Class<?> proxySuperClass = rootClass;
if (rootClass.getName().contains(ClassUtils.CGLIB_CLASS_SEPARATOR)) {
proxySuperClass = rootClass.getSuperclass();
Class<?>[] additionalInterfaces = rootClass.getInterfaces();
for (Class<?> additionalInterface : additionalInterfaces) {
this.advised.addInterface(additionalInterface);
}
}

// Validate the class, writing log messages as necessary.
validateClassIfNecessary(proxySuperClass, classLoader);

// 开始生成 cglib 对象
Enhancer enhancer = createEnhancer();
if (classLoader != null) {
enhancer.setClassLoader(classLoader);
if (classLoader instanceof SmartClassLoader &&
((SmartClassLoader) classLoader).isClassReloadable(proxySuperClass)) {
enhancer.setUseCache(false);
}
}
enhancer.setSuperclass(proxySuperClass);
// 这里Spring混合了自己的两个接口:SpringProxy和Advised
enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised));
// 类加上 BySpringCGLIB 的标志
enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
enhancer.setStrategy(new ClassLoaderAwareGeneratorStrategy(classLoader));

Callback[] callbacks = getCallbacks(rootClass);
Class<?>[] types = new Class<?>[callbacks.length];
for (int x = 0; x < types.length; x++) {
types[x] = callbacks[x].getClass();
}
// fixedInterceptorMap only populated at this point, after getCallbacks call above
enhancer.setCallbackFilter(new ProxyCallbackFilter(
this.advised.getConfigurationOnlyCopy(), this.fixedInterceptorMap, this.fixedInterceptorOffset));
enhancer.setCallbackTypes(types);

// 初始化代理对象
return createProxyClassAndInstance(enhancer, callbacks);
}
catch (CodeGenerationException IllegalArgumentException ex) {
throw new AopConfigException("Could not generate CGLIB subclass of " + this.advised.getTargetClass() +
": Common causes of this problem include using a final class or a non-visible class",
ex);
}
catch (Throwable ex) {
// TargetSource.getTarget() failed
throw new AopConfigException("Unexpected AOP exception", ex);
}
}

OK,那让我们来看看调用的时候是什么情况: 现在我的对象已经变成 NoInterfaceService$$EnhancerBySpringCGLIB 类型了,那我在调用的时候不可否认的会进入到真实的对象里边去,此时我调用一下 test() 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
private static class DynamicAdvisedInterceptor implements MethodInterceptor, Serializable {

private final AdvisedSupport advised;

public DynamicAdvisedInterceptor(AdvisedSupport advised) {
this.advised = advised;
}

@Override
@Nullable
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
Object oldProxy = null;
boolean setProxyContext = false;
Object target = null;
// 这里才是我的真实对象
TargetSource targetSource = this.advised.getTargetSource();
try {
if (this.advised.exposeProxy) {
// Make invocation available if necessary.
oldProxy = AopContext.setCurrentProxy(proxy);
setProxyContext = true;
}
// Get as late as possible to minimize the time we "own" the target, in case it comes from a pool...
target = targetSource.getTarget();
Class<?> targetClass = (target != null ? target.getClass() : null);
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
Object retVal;
// 然后拿到了我们上面装配的 Advisor 执行链条
if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) {
// 如果链条是空的并且方法是共有的话,直接执行方法!
Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
retVal = methodProxy.invoke(target, argsToUse);
}
else {
// 使用 CglibMethodInvocation 对象执行我们的方法
retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();
}
// 如果方法有返回值,转换为指定的类型.
retVal = processReturnType(proxy, target, method, retVal);
return retVal;
}
finally {
if (target != null && !targetSource.isStatic()) {
targetSource.releaseTarget(target);
}
if (setProxyContext) {
// Restore old proxy.
AopContext.setCurrentProxy(oldProxy);
}
}
}
// 省略其他代码
}

OK,接下来看看 CglibMethodInvocation#proceed() 做了什么事:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
CglibMethodInvocation#proceed:
@Override
@Nullable
public Object proceed() throws Throwable {
try {
return super.proceed();
}
catch (RuntimeException ex) {
throw ex;
}
catch (Exception ex) {
if (ReflectionUtils.declaresException(getMethod(), ex.getClass())) {
throw ex;
}
else {
throw new UndeclaredThrowableException(ex);
}
}
}
ReflectiveMethodInvocation#proceed:
@Override
@Nullable
public Object proceed() throws Throwable {
// 当在MethodAop中调用 Object proceed = joinPoint.proceed(); 的时候,此处会直接调用切入点的那个方法,就是当前的拦截器index === 最后一个的时候,是直接调用目标方法。
if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
return invokeJoinpoint();
}

Object interceptorOrInterceptionAdvice =
this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
// Evaluate dynamic method matcher here: static part will already have
// been evaluated and found to match.
InterceptorAndDynamicMethodMatcher dm =
(InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice;
Class<?> targetClass = (this.targetClass != null ? this.targetClass : this.method.getDeclaringClass());
if (dm.methodMatcher.matches(this.method, targetClass, this.arguments)) {
return dm.interceptor.invoke(this);
}
else {
// Dynamic matching failed.
// Skip this interceptor and invoke the next in the chain.
return proceed();
}
}
else {
// It's an interceptor, so we just invoke it: The pointcut will have
// been evaluated statically before this object was constructed.
return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
}
}

此时,第一个切入类是 SpringExposeInvocationInterceptor,直接走到下面的 return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public Object invoke(MethodInvocation mi) throws Throwable {
// 保存一个老的调用链
MethodInvocation oldInvocation = invocation.get();
// 当前调用链给了新的
invocation.set(mi);
try {
// 调用下一个拦截器
return mi.proceed();
}
finally {
// 恢复回去
invocation.set(oldInvocation);
}
}

重新进来还是调用 ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this) 这段代码: 此时进来的是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
AspectJAroundAdvice#invoke:
public Object invoke(MethodInvocation mi) throws Throwable {
if (!(mi instanceof ProxyMethodInvocation)) {
throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + mi);
}
ProxyMethodInvocation pmi = (ProxyMethodInvocation) mi;
// 这个参数就是我们写在 around 方法参数中的那个
// 此时我们的方法已经被织入在这个类的方法里边去
// 所以只要执行我们的 around 方法就是变相执行我们真实对象的方法了。
ProceedingJoinPoint pjp = lazyGetProceedingJoinPoint(pmi);
JoinPointMatch jpm = getJoinPointMatch(pmi);
return invokeAdviceMethod(pjp, jpm, null, null);
}
// As above, but in this case we are given the join point.
protected Object invokeAdviceMethod(JoinPoint jp, @Nullable JoinPointMatch jpMatch,
@Nullable Object returnValue, @Nullable Throwable t) throws Throwable {

return invokeAdviceMethodWithGivenArgs(argBinding(jp, jpMatch, returnValue, t));
}

protected Object invokeAdviceMethodWithGivenArgs(Object[] args) throws Throwable {
Object[] actualArgs = args;
if (this.aspectJAdviceMethod.getParameterCount() == 0) {
actualArgs = null;
}
try {
ReflectionUtils.makeAccessible(this.aspectJAdviceMethod);
// 使用反射直接调用advisor中的方法
return this.aspectJAdviceMethod.invoke(this.aspectInstanceFactory.getAspectInstance(), actualArgs);
}
catch (IllegalArgumentException ex) {
throw new AopInvocationException("Mismatch on arguments to advice method [" +
this.aspectJAdviceMethod + "]; pointcut expression [" +
this.pointcut.getPointcutExpression() + "]", ex);
}
catch (InvocationTargetException ex) {
throw ex.getTargetException();
}
}

jdk动态代理

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public Object getProxy(@Nullable ClassLoader classLoader) {
if (logger.isTraceEnabled()) {
logger.trace("Creating JDK dynamic proxy: " + this.advised.getTargetSource());
}
// 同样获取到所有的接口
// 这次 Spring 混入了三个:SpringProxy、Advised 和 DecoratingProxy
Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true);
// 获取接口中定义的 equals 方法
findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);
// 通过直接调用 jdk 的 Proxy 创建代理对象
return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
}

调用的时候则是使用 jdk 的 invoke 方式进行调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
@Override
@Nullable
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object oldProxy = null;
boolean setProxyContext = false;

TargetSource targetSource = this.advised.targetSource;
Object target = null;

try {
if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) {
// The target does not implement the equals(Object) method itself.
return equals(args[0]);
}
else if (!this.hashCodeDefined && AopUtils.isHashCodeMethod(method)) {
// The target does not implement the hashCode() method itself.
return hashCode();
}
else if (method.getDeclaringClass() == DecoratingProxy.class) {
// There is only getDecoratedClass() declared -> dispatch to proxy config.
return AopProxyUtils.ultimateTargetClass(this.advised);
}
else if (!this.advised.opaque && method.getDeclaringClass().isInterface() &&
method.getDeclaringClass().isAssignableFrom(Advised.class)) {
// Service invocations on ProxyConfig with the proxy config...
return AopUtils.invokeJoinpointUsingReflection(this.advised, method, args);
}

Object retVal;

// 上面均是特殊方法的代理形式,直接走到这里
if (this.advised.exposeProxy) {
// Make invocation available if necessary.
oldProxy = AopContext.setCurrentProxy(proxy);
setProxyContext = true;
}

// Get as late as possible to minimize the time we "own" the target,
// in case it comes from a pool.
target = targetSource.getTarget();
Class<?> targetClass = (target != null ? target.getClass() : null);

// 获取AOP拦截链
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);

// Check whether we have any advice. If we don't, we can fallback on direct
// reflective invocation of the target, and avoid creating a MethodInvocation.
if (chain.isEmpty()) {
// We can skip creating a MethodInvocation: just invoke the target directly
// Note that the final invoker must be an InvokerInterceptor so we know it does
// nothing but a reflective operation on the target, and no hot swapping or fancy proxying.
Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
}
else {
// 这一步就跟上面一样了
MethodInvocation invocation =
new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
// Proceed to the joinpoint through the interceptor chain.
retVal = invocation.proceed();
}

// Massage return value if necessary.
Class<?> returnType = method.getReturnType();
if (retVal != null && retVal == target &&
returnType != Object.class && returnType.isInstance(proxy) &&
!RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) {
// Special case: it returned "this" and the return type of the method
// is type-compatible. Note that we can't help if the target sets
// a reference to itself in another returned object.
retVal = proxy;
}
else if (retVal == null && returnType != Void.TYPE && returnType.isPrimitive()) {
throw new AopInvocationException(
"Null return value from advice does not match primitive return type for: " + method);
}
return retVal;
}
finally {
if (target != null && !targetSource.isStatic()) {
// Must have come from TargetSource.
targetSource.releaseTarget(target);
}
if (setProxyContext) {
// Restore old proxy.
AopContext.setCurrentProxy(oldProxy);
}
}
}

AOP中this的问题

这个问题是我印象最深的一个问题,曾经使用了 spring-cache,预想着通过注解,就可以解决缓存的问题,实在是一件很有感觉的事情,然而,在清理的时候,并不是这么简单…经常遇到我明明注解已经写了,但是就是没有给我清理的问题。后面就直接没有使用了。

1
2
3
4
5
6
7
8
9
10
11
12
@CacheEvict(value="userCache",key="#u.getUID()")
pubic void updateUser(User u) {
// ....逻辑
//调用其他方法让其重新缓存
userOfUID(u.getUID);
}

@Cacheable(value = "userCache",key="#result.getUID()")
pubic User userOfUID(String uid) {
userDao.userOfUID(uid);
}

然而走完 updateUser 缓存直接没了,并没有帮我刷新。 这个确实是 AOP 中模糊了 this 指向的问题。 我们通过刚刚的例子可以看到,其实 AOP 生效的时机是你从 BeanFactory 中拿到 Bean 的时候,因为这个时候你拿到的就是一个代理对象,那么会执行 AOPAdvisor 中定义的方法。但是如果我们直接在我们的类中使用 this 的时候,其实指向的是我们自己的类实例,没有走 Spring 外部的逻辑。 那咋办呢… 重点就在于怎么拿到 Spring 的代理对象!参考资料 通过配置 exposeProxy ,然后通过 ((UserService) AopContext.currentProxy()).userOfUID(); 来调用。 配置: 注解:@EnableAspectJAutoProxy(proxyTargteClass = true, exposeProxy = true) xml<aop:aspectj-autoproxy expose-proxy="true"/> 那我就比较懒(sao)了…「好了 还是不要这么用吧…」

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class UserServiceImpl implements UserService {

@Lazy
@Autowired
private UserService userService;

@CacheEvict(value="userCache",key="#u.getUID()")
pubic void updateUser(User u) {
// ....逻辑
//调用其他方法让其重新缓存
userService.userOfUID(u.getUID);
}

@Cacheable(value = "userCache",key="#result.getUID()")
pubic User userOfUID(String uid) {
userDao.userOfUID(uid);
}

}

稍微看看左上角的 BeanPostProcessor,OK,是初始化容器的时候通过他来注入 AOP 内容。

解析Advisor配置的Bean

好了,那么环境初始化好了,MethodAop 这个 Advisor 也注册进去了,接下来就是运行的时候织入 Bean 的时候了。 根据调试查看,配置中的 Bean 是在 refresh() 方法中的 finishBeanFactoryInitialization(beanFactory); 这句话来织入并且生成代理对象的。 至于怎么创建对象的,之前有说过,现在要说的只不过是创建对象以后,使用 BeanPostProcessor 把建好的对象给替换掉。 所以直接点,来到 AbstractAutowireCapableBeanFactory#doCreateBean() 看看怎么操作的。(所以无论是不是配置中的 Bean,只要使用该方法来创建的,就是该 Bean 被织入的时机)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
throws BeanCreationException {

// Instantiate the bean.
BeanWrapper instanceWrapper = null;
if (mbd.isSingleton()) {
instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
}
if (instanceWrapper == null) {
instanceWrapper = createBeanInstance(beanName, mbd, args);
}
final Object bean = instanceWrapper.getWrappedInstance();
Class<?> beanType = instanceWrapper.getWrappedClass();
if (beanType != NullBean.class) {
mbd.resolvedTargetType = beanType;
}

// Allow post-processors to modify the merged bean definition.
synchronized (mbd.postProcessingLock) {
if (!mbd.postProcessed) {
try {
applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName);
}
catch (Throwable ex) {
throw new BeanCreationException(mbd.getResourceDescription(), beanName,
"Post-processing of merged bean definition failed", ex);
}
mbd.postProcessed = true;
}
}

// Eagerly cache singletons to be able to resolve circular references
// even when triggered by lifecycle interfaces like BeanFactoryAware.
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
if (logger.isTraceEnabled()) {
logger.trace("Eagerly caching bean '" + beanName +
"' to allow for resolving potential circular references");
}
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}

// Initialize the bean instance.
Object exposedObject = bean;
try {
populateBean(beanName, mbd, instanceWrapper);
// 初始化对象,这句话会调用BeanPostProcessor的before和After的方法
// 织入的时机主要是在 postProcessAfterInitialization 方法中.
exposedObject = initializeBean(beanName, exposedObject, mbd);
}
catch (Throwable ex) {
if (ex instanceof BeanCreationException && beanName.equals(((BeanCreationException) ex).getBeanName())) {
throw (BeanCreationException) ex;
}
else {
throw new BeanCreationException(
mbd.getResourceDescription(), beanName, "Initialization of bean failed", ex);
}
}

if (earlySingletonExposure) {
Object earlySingletonReference = getSingleton(beanName, false);
if (earlySingletonReference != null) {
if (exposedObject == bean) {
exposedObject = earlySingletonReference;
}
else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
String[] dependentBeans = getDependentBeans(beanName);
Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);
for (String dependentBean : dependentBeans) {
if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
actualDependentBeans.add(dependentBean);
}
}
if (!actualDependentBeans.isEmpty()) {
throw new BeanCurrentlyInCreationException(beanName,
"Bean with name '" + beanName + "' has been injected into other beans [" +
StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +
"] in its raw version as part of a circular reference, but has eventually been " +
"wrapped. This means that said other beans do not use the final version of the " +
"bean. This is often the result of over-eager type matching - consider using " +
"'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.");
}
}
}
}

// Register bean as disposable.
try {
registerDisposableBeanIfNecessary(beanName, bean, mbd);
}
catch (BeanDefinitionValidationException ex) {
throw new BeanCreationException(
mbd.getResourceDescription(), beanName, "Invalid destruction signature", ex);
}

return exposedObject;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
class AbstractAutowireCapableBeanFactory

protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) {
if (System.getSecurityManager() != null) {
AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
invokeAwareMethods(beanName, bean);
return null;
}, getAccessControlContext());
}
else {
invokeAwareMethods(beanName, bean);
}

Object wrappedBean = bean;
if (mbd == null !mbd.isSynthetic()) {
wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
}

try {
invokeInitMethods(beanName, wrappedBean, mbd);
}
catch (Throwable ex) {
throw new BeanCreationException(
(mbd != null ? mbd.getResourceDescription() : null),
beanName, "Invocation of init method failed", ex);
}
if (mbd == null !mbd.isSynthetic()) {
// 应用Bean创建的后处理器,包装Bean
wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
}

return wrappedBean;
}

// 每个Bean创建就一顿循环 BeanPostProcessors,调用 BeanPostProcessor#postProcessAfterInitialization来处理实例
// 那么刚刚上面我们说过,环境初始化的时候,Spring偷偷放进去了一个 AnnotationAwareAspectJAutoProxyCreator
// 他其实就是一个BeanPostProcessors,所以不用说肯定在这个类里边处理
@Override
public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName)
throws BeansException {

Object result = existingBean;
for (BeanPostProcessor processor : getBeanPostProcessors()) {
Object current = processor.postProcessAfterInitialization(result, beanName);
if (current == null) {
return result;
}
result = current;
}
return result;
}

分享个 debug 小技巧,因为是个循环,然后又很难看出来他循环到哪个类,稍微手抖了一下,又跳过了这个类又要重新开始 debug。所以我们可以使用 idea 提供的条件断点,我这里是使用 class 来比较,如果是我想要的这个 class 那么就停止在这个断点:

前言

关于 AOP 在日常开发中应该不算是一个新的名词了,AOP 可以帮助我们把业务之外,但是很多方法都需要调用的方法,无感的方外外部,并且通过配置让外部的 AOP 函数自动织入目标方法中。 常见的业务有:

  1. 记录方法调用的信息,时长;
  2. 初始化当前用户信息上下文,方便在业务内部快速获取。

那我就改改我之前的 Context 用的例子,来康康 Spring 是怎么做的。

准备

applicationContext.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
">

<bean class="cn.liweidan.confbean.MyBeanConfiguration"/>
<context:annotation-config/>
<!-- 开启自动代理支持 -->
<aop:aspectj-autoproxy/>

</beans>

MyBeanConfiguration.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Configurable
public class MyBeanConfiguration {

// @Bean
// public MyBean myBean() {
// MyBean myBean = new MyBean();
// myBean.setName("Weidan");
// return myBean;
// }

@Bean
public NoInterfaceService noInterfaceService() {
return new NoInterfaceService();
}

@Bean
public MethodAop methodAop() {
return new MethodAop();
}

@Bean
public HasInterfaceService hasInterfaceService() {
return new HasInterfaceServiceImpl();
}

}

一个没有接口实现的业务层:

1
2
3
4
5
6
7
public class NoInterfaceService {

public void test() {
System.out.println("这是方法里面的内容");
}

}

一个有接口实现的业务层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface HasInterfaceService {

void testHasInterface();

}

public class HasInterfaceServiceImpl implements HasInterfaceService {

@Override
public void testHasInterface() {
System.out.println("一个有接口的方法的内部");
}

}

一个注解方式的自动织入 Bean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Aspect
public class MethodAop {

@Pointcut("execution(* cn.liweidan.confbean.service.*.*(..)) execution(* cn.liweidan.confbean.services.impl.*.*(..))")
public void aopPoint() {}

@Around("aopPoint()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println(joinPoint.getSignature().getDeclaringTypeName() + "方法开始执行");
Object proceed = joinPoint.proceed();
System.out.println(joinPoint.getSignature().getDeclaringTypeName() + "方法完成执行");
return proceed;
}

}

测试用例:

1
2
3
4
5
6
7
8
9
10
11
12
public class ConfigBeanTest {

@Test
public void testAop() {
BeanFactory bf = new ClassPathXmlApplicationContext("applicationContext.xml");
NoInterfaceService bean = bf.getBean(NoInterfaceService.class);
bean.test();
HasInterfaceService hasInterfaceService = bf.getBean(HasInterfaceService.class);
hasInterfaceService.testHasInterface();
}

}

试运行一下:

1
2
3
4
5
6
cn.liweidan.confbean.service.NoInterfaceService方法开始执行
这是方法里面的内容
cn.liweidan.confbean.service.NoInterfaceService方法完成执行
cn.liweidan.confbean.services.HasInterfaceService方法开始执行
一个有接口的方法的内部
cn.liweidan.confbean.services.HasInterfaceService方法完成执行

好了成功!

注册标签处理器

之前几篇我都直接跳过 xml 解析,这次终于跳不过去了。因为在处理器这一块,Spring 确实能够让我特别惊喜,模块间的解耦仿佛就是我不知道你有什么东西我也不关心但是最后我就能够调用到你的傲娇姑娘的任性。 让我们回到第一篇的 BeanDefinitionDocumentReader读取配置文档 这一节中(传送门),我重新贴一下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
@SuppressWarnings("deprecation")  // for Environment.acceptsProfiles(String...)
protected void doRegisterBeanDefinitions(Element root) {
// Any nested <beans> elements will cause recursion in this method. In
// order to propagate and preserve <beans> default-* attributes correctly,
// keep track of the current (parent) delegate, which may be null. Create
// the new (child) delegate with a reference to the parent for fallback purposes,
// then ultimately reset this.delegate back to its original (parent) reference.
// this behavior emulates a stack of delegates without actually necessitating one.
BeanDefinitionParserDelegate parent = this.delegate;
this.delegate = createDelegate(getReaderContext(), root, parent);

if (this.delegate.isDefaultNamespace(root)) {
String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
if (StringUtils.hasText(profileSpec)) {
String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
// We cannot use Profiles.of(...) since profile expressions are not supported
// in XML config. See SPR-12458 for details.
if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
if (logger.isDebugEnabled()) {
logger.debug("Skipped XML bean definition file due to specified profiles [" + profileSpec +
"] not matching: " + getReaderContext().getResource());
}
return;
}
}
}

preProcessXml(root);
// 来到这句话中,因为这句话会解析到配置文件中的 <aop:aspectj-autoproxy/> 元素中。
parseBeanDefinitions(root, this.delegate);
postProcessXml(root);

this.delegate = parent;
}

// 在这个方法中实现解析:
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
if (delegate.isDefaultNamespace(root)) {
NodeList nl = root.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
if (node instanceof Element) {
Element ele = (Element) node;
if (delegate.isDefaultNamespace(ele)) {
parseDefaultElement(ele, delegate);
}
else {
// 因为 <aop:aspectj-autoproxy/> 并不是默认的命名空间(默认只有:http://www.springframework.org/schema/beans),
// 所以需要调用自定义的处理器来解析元素.
delegate.parseCustomElement(ele);
}
}
}
}
else {
delegate.parseCustomElement(root);
}
}

/**
* Parse a custom element (outside of the default namespace).
* @param ele the element to parse
* @return the resulting bean definition
*/
@Nullable
public BeanDefinition parseCustomElement(Element ele) {
return parseCustomElement(ele, null);
}

/**
* Parse a custom element (outside of the default namespace).
* @param ele the element to parse
* @param containingBd the containing bean definition (if any)
* @return the resulting bean definition
*/
@Nullable
public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) {
// AOP的命名空间是http://www.springframework.org/schema/aop
String namespaceUri = getNamespaceURI(ele);
if (namespaceUri == null) {
return null;
}
// 开始从上下文中寻找合适的解析器,第一个方法只是简单地返回一个实例
// 那下面直接看看 resolve 怎么处理的
NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
if (handler == null) {
error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele);
return null;
}
return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
}

在寻找解析器这一步就挺骚了,直接从所有模块中的 META-INF/spring.handlers 中开始查找其他模块配置的解析器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
@Override
@Nullable
public NamespaceHandler resolve(String namespaceUri) {
// 获取所有的解析器,依赖的所有 jar 包都会读取
Map<String, Object> handlerMappings = getHandlerMappings();
// 因为获取的是字符串,所以需要通过反射实例化解析器对象
Object handlerOrClassName = handlerMappings.get(namespaceUri);
if (handlerOrClassName == null) {
return null;
}
else if (handlerOrClassName instanceof NamespaceHandler) {
return (NamespaceHandler) handlerOrClassName;
}
else {
String className = (String) handlerOrClassName;
try {
Class<?> handlerClass = ClassUtils.forName(className, this.classLoader);
if (!NamespaceHandler.class.isAssignableFrom(handlerClass)) {
throw new FatalBeanException("Class [" + className + "] for namespace [" + namespaceUri +
"] does not implement the [" + NamespaceHandler.class.getName() + "] interface");
}
// 实例化对象
NamespaceHandler namespaceHandler = (NamespaceHandler) BeanUtils.instantiateClass(handlerClass);
namespaceHandler.init();
// 覆盖之前的字符串,以便后面如果还需要可以直接返回对象。
handlerMappings.put(namespaceUri, namespaceHandler);
return namespaceHandler;
}
catch (ClassNotFoundException ex) {
throw new FatalBeanException("Could not find NamespaceHandler class [" + className +
"] for namespace [" + namespaceUri + "]", ex);
}
catch (LinkageError err) {
throw new FatalBeanException("Unresolvable class definition for NamespaceHandler class [" +
className + "] for namespace [" + namespaceUri + "]", err);
}
}
}

/**
* 惰性读取所有解析器.
*/
private Map<String, Object> getHandlerMappings() {
Map<String, Object> handlerMappings = this.handlerMappings;
if (handlerMappings == null) {
synchronized (this) {
handlerMappings = this.handlerMappings;
if (handlerMappings == null) {
if (logger.isTraceEnabled()) {
logger.trace("Loading NamespaceHandler mappings from [" + this.handlerMappingsLocation + "]");
}
try {
// 这一步就是从配置的 handlerMappingsLocation=DEFAULT_HANDLER_MAPPINGS_LOCATION = "META-INF/spring.handlers" 中去查找其他模块编写的配置文件,然后获取到对象的自定义标签的解析器
Properties mappings =
PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);
if (logger.isTraceEnabled()) {
logger.trace("Loaded NamespaceHandler mappings: " + mappings);
}
// 开始加载解析器,然后缓存起来返回到上面的方法中。
handlerMappings = new ConcurrentHashMap<>(mappings.size());
CollectionUtils.mergePropertiesIntoMap(mappings, handlerMappings);
this.handlerMappings = handlerMappings;
}
catch (IOException ex) {
throw new IllegalStateException(
"Unable to load NamespaceHandler mappings from location [" + this.handlerMappingsLocation + "]", ex);
}
}
}
}
return handlerMappings;
}

AOP模块中的 META-INF/spring.handlers

1
http\://www.springframework.org/schema/aop=org.springframework.aop.config.AopNamespaceHandler

然后就可以开始使用其他模块的处理器开始处理元素了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Nullable
public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) {
// AOP的命名空间是http://www.springframework.org/schema/aop
String namespaceUri = getNamespaceURI(ele);
if (namespaceUri == null) {
return null;
}
// 开始从上下文中寻找合适的解析器,第一个方法只是简单地返回一个实例
// 那下面直接看看 resolve 怎么处理的
NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
if (handler == null) {
error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele);
return null;
}
// 开始处理
return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
}

初始化AOP所需环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// NamespaceHandler内的方法:
@Override
@Nullable
public BeanDefinition parse(Element element, ParserContext parserContext) {
BeanDefinitionParser parser = findParserForElement(element, parserContext);
return (parser != null ? parser.parse(element, parserContext) : null);
}
// 还有一个抽象的方法
@Nullable
BeanDefinition parse(Element element, ParserContext parserContext);

//AopNamespaceHandler处理器的实现:
@Override
@Nullable
public BeanDefinition parse(Element element, ParserContext parserContext) {
AopNamespaceUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(parserContext, element);
extendBeanDefinition(element, parserContext);
return null;
}

首先看看 AopNamespaceUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(parserContext, element); 这句话,看方法名初步判断是注册一个 BeanDefinition 以便后续用来解析基于 Java注解AOP Advisor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public static void registerAspectJAnnotationAutoProxyCreatorIfNecessary(
ParserContext parserContext, Element sourceElement) {

BeanDefinition beanDefinition = AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(
parserContext.getRegistry(), parserContext.extractSource(sourceElement));
// 因为只是简单配置标签,没有定义属性,所以这个方法里面是个空的执行
useClassProxyingIfNecessary(parserContext.getRegistry(), sourceElement);
// 将第一句话拿到的 beanDefinition 注册到 bf 中去,使用 Component 组的形式进行注册。
registerComponentIfNecessary(beanDefinition, parserContext);
}
// 调用了这里:
@Nullable
public static BeanDefinition registerAspectJAnnotationAutoProxyCreatorIfNecessary(
BeanDefinitionRegistry registry, @Nullable Object source) {

return registerOrEscalateApcAsRequired(AnnotationAwareAspectJAutoProxyCreator.class, registry, source);
}
// 然后再继续调用了这里:
@Nullable
private static BeanDefinition registerOrEscalateApcAsRequired(
Class<?> cls, BeanDefinitionRegistry registry, @Nullable Object source) {

Assert.notNull(registry, "BeanDefinitionRegistry must not be null");

if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) {
BeanDefinition apcDefinition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME);
if (!cls.getName().equals(apcDefinition.getBeanClassName())) {
int currentPriority = findPriorityForClass(apcDefinition.getBeanClassName());
int requiredPriority = findPriorityForClass(cls);
if (currentPriority < requiredPriority) {
apcDefinition.setBeanClassName(cls.getName());
}
}
return null;
}

RootBeanDefinition beanDefinition = new RootBeanDefinition(cls);
beanDefinition.setSource(source);
beanDefinition.getPropertyValues().add("order", Ordered.HIGHEST_PRECEDENCE);
beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
registry.registerBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME, beanDefinition);
return beanDefinition;
}

果然就是注册了一个 AnnotationAwareAspectJAutoProxyCreator 这个东西,用来创建代理的玩意儿。 先看看这个类的继承体系:

稍微看看左上角的 BeanPostProcessor,OK,是初始化容器的时候通过他来注入 AOP 内容。

解析Advisor配置的Bean

好了,那么环境初始化好了,MethodAop 这个 Advisor 也注册进去了,接下来就是运行的时候织入 Bean 的时候了。 根据调试查看,配置中的 Bean 是在 refresh() 方法中的 finishBeanFactoryInitialization(beanFactory); 这句话来织入并且生成代理对象的。 至于怎么创建对象的,之前有说过,现在要说的只不过是创建对象以后,使用 BeanPostProcessor 把建好的对象给替换掉。 所以直接点,来到 AbstractAutowireCapableBeanFactory#doCreateBean() 看看怎么操作的。(所以无论是不是配置中的 Bean,只要使用该方法来创建的,就是该 Bean 被织入的时机)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
throws BeanCreationException {

// Instantiate the bean.
BeanWrapper instanceWrapper = null;
if (mbd.isSingleton()) {
instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
}
if (instanceWrapper == null) {
instanceWrapper = createBeanInstance(beanName, mbd, args);
}
final Object bean = instanceWrapper.getWrappedInstance();
Class<?> beanType = instanceWrapper.getWrappedClass();
if (beanType != NullBean.class) {
mbd.resolvedTargetType = beanType;
}

// Allow post-processors to modify the merged bean definition.
synchronized (mbd.postProcessingLock) {
if (!mbd.postProcessed) {
try {
applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName);
}
catch (Throwable ex) {
throw new BeanCreationException(mbd.getResourceDescription(), beanName,
"Post-processing of merged bean definition failed", ex);
}
mbd.postProcessed = true;
}
}

// Eagerly cache singletons to be able to resolve circular references
// even when triggered by lifecycle interfaces like BeanFactoryAware.
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
if (logger.isTraceEnabled()) {
logger.trace("Eagerly caching bean '" + beanName +
"' to allow for resolving potential circular references");
}
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}

// Initialize the bean instance.
Object exposedObject = bean;
try {
populateBean(beanName, mbd, instanceWrapper);
// 初始化对象,这句话会调用BeanPostProcessor的before和After的方法
// 织入的时机主要是在 postProcessAfterInitialization 方法中.
exposedObject = initializeBean(beanName, exposedObject, mbd);
}
catch (Throwable ex) {
if (ex instanceof BeanCreationException && beanName.equals(((BeanCreationException) ex).getBeanName())) {
throw (BeanCreationException) ex;
}
else {
throw new BeanCreationException(
mbd.getResourceDescription(), beanName, "Initialization of bean failed", ex);
}
}

if (earlySingletonExposure) {
Object earlySingletonReference = getSingleton(beanName, false);
if (earlySingletonReference != null) {
if (exposedObject == bean) {
exposedObject = earlySingletonReference;
}
else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
String[] dependentBeans = getDependentBeans(beanName);
Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);
for (String dependentBean : dependentBeans) {
if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
actualDependentBeans.add(dependentBean);
}
}
if (!actualDependentBeans.isEmpty()) {
throw new BeanCurrentlyInCreationException(beanName,
"Bean with name '" + beanName + "' has been injected into other beans [" +
StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +
"] in its raw version as part of a circular reference, but has eventually been " +
"wrapped. This means that said other beans do not use the final version of the " +
"bean. This is often the result of over-eager type matching - consider using " +
"'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.");
}
}
}
}

// Register bean as disposable.
try {
registerDisposableBeanIfNecessary(beanName, bean, mbd);
}
catch (BeanDefinitionValidationException ex) {
throw new BeanCreationException(
mbd.getResourceDescription(), beanName, "Invalid destruction signature", ex);
}

return exposedObject;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
class AbstractAutowireCapableBeanFactory

protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) {
if (System.getSecurityManager() != null) {
AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
invokeAwareMethods(beanName, bean);
return null;
}, getAccessControlContext());
}
else {
invokeAwareMethods(beanName, bean);
}

Object wrappedBean = bean;
if (mbd == null !mbd.isSynthetic()) {
wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
}

try {
invokeInitMethods(beanName, wrappedBean, mbd);
}
catch (Throwable ex) {
throw new BeanCreationException(
(mbd != null ? mbd.getResourceDescription() : null),
beanName, "Invocation of init method failed", ex);
}
if (mbd == null !mbd.isSynthetic()) {
// 应用Bean创建的后处理器,包装Bean
wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
}

return wrappedBean;
}

// 每个Bean创建就一顿循环 BeanPostProcessors,调用 BeanPostProcessor#postProcessAfterInitialization来处理实例
// 那么刚刚上面我们说过,环境初始化的时候,Spring偷偷放进去了一个 AnnotationAwareAspectJAutoProxyCreator
// 他其实就是一个BeanPostProcessors,所以不用说肯定在这个类里边处理
@Override
public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName)
throws BeansException {

Object result = existingBean;
for (BeanPostProcessor processor : getBeanPostProcessors()) {
Object current = processor.postProcessAfterInitialization(result, beanName);
if (current == null) {
return result;
}
result = current;
}
return result;
}

分享个 debug 小技巧,因为是个循环,然后又很难看出来他循环到哪个类,稍微手抖了一下,又跳过了这个类又要重新开始 debug。所以我们可以使用 idea 提供的条件断点,我这里是使用 class 来比较,如果是我想要的这个 class 那么就停止在这个断点:

接下来进入看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* Create a proxy with the configured interceptors if the bean is
* identified as one to proxy by the subclass.
* @see #getAdvicesAndAdvisorsForBean
*/
@Override
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
if (bean != null) {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
// 提早曝光的代理引用,如果不是相同的,则调用 wrapIfNecessary 进行包装
if (this.earlyProxyReferences.remove(cacheKey) != bean) {
return wrapIfNecessary(bean, beanName, cacheKey);
}
}
return bean;
}
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
return bean;
}
// 这里通过 cacheKey 判断当前 Bean 是否之前记录不需要切入(使用 advisedBeans 存储)
if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
return bean;
}
// 1. 如果是 Advice/Pointcut 等 AOP 相关的类则跳过;
// 2. BeanName以.ORIGINAL结尾的话,不做代理,如:com.mypackage.MyClass.ORIGINAL
if (isInfrastructureClass(bean.getClass()) shouldSkip(bean.getClass(), beanName)) {
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}

// 在这里开始获取 Advisor
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
if (specificInterceptors != DO_NOT_PROXY) {
this.advisedBeans.put(cacheKey, Boolean.TRUE);
// 开始创建代理对象
Object proxy = createProxy(
bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
this.proxyTypes.put(cacheKey, proxy.getClass());
return proxy;
}

this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}

获取advisor

如果暂时对如何获取 advisor 没兴趣的话,可以先跳过,后面再回来看

1
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);

这句话是用来获取 advisor 的,并且缓存起来项目中配置的 advisor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Override
@Nullable
protected Object[] getAdvicesAndAdvisorsForBean(
Class<?> beanClass, String beanName, @Nullable TargetSource targetSource) {

List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName);
if (advisors.isEmpty()) {
return DO_NOT_PROXY;
}
return advisors.toArray();
}

protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
List<Advisor> candidateAdvisors = findCandidateAdvisors();
List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
extendAdvisors(eligibleAdvisors);
if (!eligibleAdvisors.isEmpty()) {
eligibleAdvisors = sortAdvisors(eligibleAdvisors);
}
return eligibleAdvisors;
}

List<Advisor> candidateAdvisors = findCandidateAdvisors(); 这句话使用来获取已经缓存起来了的所有 Advisors。因为在当前我们已经无法进入到怎么解析并缓存的问题上来了(因为已经缓存起来了),所以我们需要退回到前面的步骤中来。

解析Advisor

然而什么时候做的解析和缓存,我们刚刚在上一节中说了 initializeBean 会在创建所有 Bean 的时候调用所有 beanPostProcessor 的前置处理器。那我们来看看 AnnotationAwareAspectJAutoProxyCreator 的前置处理器做了什么,并且第一次调用该方法是什么时候:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
@Override
public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) {
Object cacheKey = getCacheKey(beanClass, beanName);

// 这里在前置的时候就开始记录哪些 Bean 需要织入,哪些不需要了
if (!StringUtils.hasLength(beanName) !this.targetSourcedBeans.contains(beanName)) {
if (this.advisedBeans.containsKey(cacheKey)) {
return null;
}
// 初始化第一个 Bean(在我这里是我那个配置类的初始化)的时候就开始调用 shouldSkip
if (isInfrastructureClass(beanClass) shouldSkip(beanClass, beanName)) {
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return null;
}
}

// Create proxy here if we have a custom TargetSource.
// Suppresses unnecessary default instantiation of the target bean:
// The TargetSource will handle target instances in a custom fashion.
TargetSource targetSource = getCustomTargetSource(beanClass, beanName);
if (targetSource != null) {
if (StringUtils.hasLength(beanName)) {
this.targetSourcedBeans.add(beanName);
}
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(beanClass, beanName, targetSource);
Object proxy = createProxy(beanClass, beanName, specificInterceptors, targetSource);
this.proxyTypes.put(cacheKey, proxy.getClass());
return proxy;
}

return null;
}
@Override
protected boolean shouldSkip(Class<?> beanClass, String beanName) {
// 获取当前 BeanFactory 中所有的 advisor 对象,并且做缓存
List<Advisor> candidateAdvisors = findCandidateAdvisors();
for (Advisor advisor : candidateAdvisors) {
if (advisor instanceof AspectJPointcutAdvisor &&
((AspectJPointcutAdvisor) advisor).getAspectName().equals(beanName)) {
return true;
}
}
return super.shouldSkip(beanClass, beanName);
}

protected List<Advisor> findCandidateAdvisors() {
// 先调用下面那个在父类的方法
List<Advisor> advisors = super.findCandidateAdvisors();
// 开始构建存在 BeanFactory 中的 Advisor
if (this.aspectJAdvisorsBuilder != null) {
advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors());
}
return advisors;
}
protected List<Advisor> findCandidateAdvisors() {
Assert.state(this.advisorRetrievalHelper != null, "No BeanFactoryAdvisorRetrievalHelper available");
// 通过一个 advisorRetrievalHelper 来获取所有的 advisor Bean
return this.advisorRetrievalHelper.findAdvisorBeans();
}

advisorRetrievalHelper 是配置 auto-proxy 的时候,会被使用的一个类,他在 AnnotationAwareAspectJAutoProxyCreator 中传入一个 ConfigurableListableBeanFactory 作为参数调用构造器初始化。 所以,advisorRetrievalHelper 可以在当前容器中,查找所有的 BeanDefinition,并且取出是 AdvisorBean。 接下来来看看这个方法做了什么事情:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* Find all eligible Advisor beans in the current bean factory,
* ignoring FactoryBeans and excluding beans that are currently in creation.
* @return the list of {@link org.springframework.aop.Advisor} beans
* @see #isEligibleBean
*/
public List<Advisor> findAdvisorBeans() {
// Determine list of advisor bean names, if not cached already.
String[] advisorNames = this.cachedAdvisorBeanNames;
if (advisorNames == null) {
// Do not initialize FactoryBeans here: We need to leave all regular beans
// uninitialized to let the auto-proxy creator apply to them!
advisorNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
this.beanFactory, Advisor.class, true, false);
this.cachedAdvisorBeanNames = advisorNames;
}
// 第一次调用的时候,还没找到,所以直接就在这里被返回出去
if (advisorNames.length == 0) {
return new ArrayList<>();
}

List<Advisor> advisors = new ArrayList<>();
for (String name : advisorNames) {
if (isEligibleBean(name)) {
if (this.beanFactory.isCurrentlyInCreation(name)) {
if (logger.isTraceEnabled()) {
logger.trace("Skipping currently created advisor '" + name + "'");
}
}
else {
try {
advisors.add(this.beanFactory.getBean(name, Advisor.class));
}
catch (BeanCreationException ex) {
.....
continue;
}
}
throw ex;
}
}
}
}
return advisors;
}

OK,既然返回了一个空的集合,那么就来看看,后面做了什么事情了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
protected List<Advisor> findCandidateAdvisors() {
// 先调用下面那个在父类的方法
List<Advisor> advisors = super.findCandidateAdvisors();
// 开始构建存在 BeanFactory 中的 Advisor
if (this.aspectJAdvisorsBuilder != null) {
advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors());
}
return advisors;
}
public List<Advisor> buildAspectJAdvisors() {
List<String> aspectNames = this.aspectBeanNames;

if (aspectNames == null) {
synchronized (this) {
// 因为 aspectBeanNames 现在是 NULL,所以进入开始初始化
aspectNames = this.aspectBeanNames;
if (aspectNames == null) {
List<Advisor> advisors = new ArrayList<>();
aspectNames = new ArrayList<>();
// 获取 BeanFactory 中所有的 beanNames
String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
this.beanFactory, Object.class, true, false);
// 二话不说就来了个循环
for (String beanName : beanNames) {
// 是否是个有资格的类?然而方法都是直接返回 true...也就是这一步永远不会进去
// 感觉这个方法是个有故事的方法,先不看
if (!isEligibleBean(beanName)) {
continue;
}
// 这一步必须使用 getType 调用 getBean 可能会导致错误(那个Bean需要被织入但是太早初始化的
// 话会导致没有织入成功)
Class<?> beanType = this.beanFactory.getType(beanName);
if (beanType == null) {
continue;
}
// 这一步会判断类有没有被 @Aspect 修饰
if (this.advisorFactory.isAspect(beanType)) {
aspectNames.add(beanName);
AspectMetadata amd = new AspectMetadata(beanType, beanName);
// 根据 Advisor 是否可以为单例(这个是AOP定义的单例)来缓存 AOP 信息以便后面可以使用
if (amd.getAjType().getPerClause().getKind() == PerClauseKind.SINGLETON) {
// 构建 Advisor 工厂
MetadataAwareAspectInstanceFactory factory =
new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName);
List<Advisor> classAdvisors = this.advisorFactory.getAdvisors(factory);
// 如果是单例(BeanFactory的单例)的 Bean,则直接缓存 Advisor 对象
if (this.beanFactory.isSingleton(beanName)) {
this.advisorsCache.put(beanName, classAdvisors);
}
else {
this.aspectFactoryCache.put(beanName, factory);
}
advisors.addAll(classAdvisors);
}
else {
// 每个目标对象或者每个对象都去织入.
if (this.beanFactory.isSingleton(beanName)) {
throw new IllegalArgumentException("Bean with name '" + beanName +
"' is a singleton, but aspect instantiation model is not singleton");
}
MetadataAwareAspectInstanceFactory factory =
new PrototypeAspectInstanceFactory(this.beanFactory, beanName);
this.aspectFactoryCache.put(beanName, factory);
advisors.addAll(this.advisorFactory.getAdvisors(factory));
}
}
}
this.aspectBeanNames = aspectNames;
return advisors;
}
}
}

// 下一次再调用该方法的时候,即可直接通过缓存中取出 Advisor 使用
if (aspectNames.isEmpty()) {
return Collections.emptyList();
}
List<Advisor> advisors = new ArrayList<>();
for (String aspectName : aspectNames) {
List<Advisor> cachedAdvisors = this.advisorsCache.get(aspectName);
if (cachedAdvisors != null) {
advisors.addAll(cachedAdvisors);
}
else {
MetadataAwareAspectInstanceFactory factory = this.aspectFactoryCache.get(aspectName);
advisors.addAll(this.advisorFactory.getAdvisors(factory));
}
}
return advisors;
}

获取所有的Advisor

刚刚我们来到这里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
@Nullable
protected Object[] getAdvicesAndAdvisorsForBean(
Class<?> beanClass, String beanName, @Nullable TargetSource targetSource) {

List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName);
if (advisors.isEmpty()) {
return DO_NOT_PROXY;
}
return advisors.toArray();
}

protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
List<Advisor> candidateAdvisors = findCandidateAdvisors();// <-----
// 获取装配当前 Bean 的 Advisor
// 这一步其实就是获取 @Pointcut 中配置的装配规则来匹配所有的方法,我偷懒,先过
List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
// 扩展Advisor
extendAdvisors(eligibleAdvisors);
if (!eligibleAdvisors.isEmpty()) {
eligibleAdvisors = sortAdvisors(eligibleAdvisors);
}
return eligibleAdvisors;
}

现在在这个方法内部,已经获取我们自己配置的 MethodAop 对象了,但是下面还有个扩展,看看扩展了什么东西:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* Adds an {@link ExposeInvocationInterceptor} to the beginning of the advice chain.
* These additional advices are needed when using AspectJ expression pointcuts
* and when using AspectJ-style advice.
*/
@Override
protected void extendAdvisors(List<Advisor> candidateAdvisors) {
AspectJProxyUtils.makeAdvisorChainAspectJCapableIfNecessary(candidateAdvisors);
}

public static boolean makeAdvisorChainAspectJCapableIfNecessary(List<Advisor> advisors) {
// Don't add advisors to an empty list; may indicate that proxying is just not required
if (!advisors.isEmpty()) {
boolean foundAspectJAdvice = false;
for (Advisor advisor : advisors) {
// 如果是 AspectJAdvice 则不添加
if (isAspectJAdvice(advisor)) {
foundAspectJAdvice = true;
break;
}
}
// 我们织入的方法中,都会添加一个 ExposeInvocationInterceptor 实例
if (foundAspectJAdvice && !advisors.contains(ExposeInvocationInterceptor.ADVISOR)) {
advisors.add(0, ExposeInvocationInterceptor.ADVISOR);
return true;
}
}
return false;
}

这里有两个概念:

  1. AspectJAdvice:通过编译器织入 AOP 代码的一个框架;

  2. ExposeInvocationInterceptor:这是一个运行期织入时可以随时获取整个调用链的内置的 AOP 拦截器,如果在使用过程中需要获取调用链的话,可以通过 ExposeInvocationInterceptor.currentInvocation 来获取。SpringAOP 在织入的时候默认在第一个织入这个实例。

好了,走完全部,获取所有 advisors 的过程终于完成。

织入运行期对象

回到 postProcessAfterInitialization -> wrapIfNecessary 方法中来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
return bean;
}
// 这里通过 cacheKey 判断当前 Bean 是否之前记录不需要切入(使用 advisedBeans 存储)
if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
return bean;
}
// 1. 如果是 Advice/Pointcut 等 AOP 相关的类则跳过;
// 2. BeanName以.ORIGINAL结尾的话,不做代理,如:com.mypackage.MyClass.ORIGINAL
if (isInfrastructureClass(bean.getClass()) shouldSkip(bean.getClass(), beanName)) {
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}

// 在这里开始获取 Advisor
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
if (specificInterceptors != DO_NOT_PROXY) {
this.advisedBeans.put(cacheKey, Boolean.TRUE);
// 开始创建代理对象
Object proxy = createProxy(
bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
this.proxyTypes.put(cacheKey, proxy.getClass());
return proxy;
}

this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}

接下来就是开始创建代理对象了。创建对象有两种方式:cglibjdk代理,使用哪种方式取决于当前那个类有没有接口实现,如果是一个普通的类(没有实现接口的方式),则使用前者,否则用后者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
@Nullable Object[] specificInterceptors, TargetSource targetSource) {

if (this.beanFactory instanceof ConfigurableListableBeanFactory) {
// 记录原始数据
AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass);
}

// 创建 ProxyFactory 用于创建代理
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.copyFrom(this);

if (!proxyFactory.isProxyTargetClass()) {
if (shouldProxyTargetClass(beanClass, beanName)) {
// 直接代理类实例(通过BeanDefinition配置的属性来判断)
proxyFactory.setProxyTargetClass(true);
}
else {
// 计算所有的接口设置到工厂,如果没有接口,同上直接代理实例
evaluateProxyInterfaces(beanClass, proxyFactory);
}
}

// 通过上一步获取的所有 advisors,过滤获取匹配的 advisors
Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
proxyFactory.addAdvisors(advisors);
proxyFactory.setTargetSource(targetSource);
customizeProxyFactory(proxyFactory);

proxyFactory.setFrozen(this.freezeProxy);
if (advisorsPreFiltered()) {
proxyFactory.setPreFiltered(true);
}

// 开始构建
return proxyFactory.getProxy(getProxyClassLoader());
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public Object getProxy(@Nullable ClassLoader classLoader) {
// 调用 DefaultAopProxyFactory 开始构建代理
return createAopProxy().getProxy(classLoader);
}
protected final synchronized AopProxy createAopProxy() {
if (!this.active) {
activate();
}
// getAopProxyFactory返回DefaultAopProxyFactory实例
return getAopProxyFactory().createAopProxy(this);
}
DefaultAopProxyFactory#createAopProxy
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
if (config.isOptimize() config.isProxyTargetClass() hasNoUserSuppliedProxyInterfaces(config)) {
Class<?> targetClass = config.getTargetClass();
if (targetClass == null) {
throw new AopConfigException("TargetSource cannot determine target class: " +
"Either an interface or a target is required for proxy creation.");
}
if (targetClass.isInterface() Proxy.isProxyClass(targetClass)) {
return new JdkDynamicAopProxy(config);
}
// 如果没有接口,来到这
return new ObjenesisCglibAopProxy(config);
}
else {
return new JdkDynamicAopProxy(config);
}
}

createAopProxy 之后就是调用各自的实现 getProxy(classLoader)

Cglib

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
@Override
public Object getProxy(@Nullable ClassLoader classLoader) {
if (logger.isTraceEnabled()) {
logger.trace("Creating CGLIB proxy: " + this.advised.getTargetSource());
}

try {
// 获取对象的实际类型
Class<?> rootClass = this.advised.getTargetClass();
Assert.state(rootClass != null, "Target class must be available for creating a CGLIB proxy");

// 如果已经是一个 cglib 对象的时候,获取其父级(实际类型)
Class<?> proxySuperClass = rootClass;
if (rootClass.getName().contains(ClassUtils.CGLIB_CLASS_SEPARATOR)) {
proxySuperClass = rootClass.getSuperclass();
Class<?>[] additionalInterfaces = rootClass.getInterfaces();
for (Class<?> additionalInterface : additionalInterfaces) {
this.advised.addInterface(additionalInterface);
}
}

// Validate the class, writing log messages as necessary.
validateClassIfNecessary(proxySuperClass, classLoader);

// 开始生成 cglib 对象
Enhancer enhancer = createEnhancer();
if (classLoader != null) {
enhancer.setClassLoader(classLoader);
if (classLoader instanceof SmartClassLoader &&
((SmartClassLoader) classLoader).isClassReloadable(proxySuperClass)) {
enhancer.setUseCache(false);
}
}
enhancer.setSuperclass(proxySuperClass);
// 这里Spring混合了自己的两个接口:SpringProxy和Advised
enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised));
// 类加上 BySpringCGLIB 的标志
enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
enhancer.setStrategy(new ClassLoaderAwareGeneratorStrategy(classLoader));

Callback[] callbacks = getCallbacks(rootClass);
Class<?>[] types = new Class<?>[callbacks.length];
for (int x = 0; x < types.length; x++) {
types[x] = callbacks[x].getClass();
}
// fixedInterceptorMap only populated at this point, after getCallbacks call above
enhancer.setCallbackFilter(new ProxyCallbackFilter(
this.advised.getConfigurationOnlyCopy(), this.fixedInterceptorMap, this.fixedInterceptorOffset));
enhancer.setCallbackTypes(types);

// 初始化代理对象
return createProxyClassAndInstance(enhancer, callbacks);
}
catch (CodeGenerationException IllegalArgumentException ex) {
throw new AopConfigException("Could not generate CGLIB subclass of " + this.advised.getTargetClass() +
": Common causes of this problem include using a final class or a non-visible class",
ex);
}
catch (Throwable ex) {
// TargetSource.getTarget() failed
throw new AopConfigException("Unexpected AOP exception", ex);
}
}

OK,那让我们来看看调用的时候是什么情况: 现在我的对象已经变成 NoInterfaceService$$EnhancerBySpringCGLIB 类型了,那我在调用的时候不可否认的会进入到真实的对象里边去,此时我调用一下 test() 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
private static class DynamicAdvisedInterceptor implements MethodInterceptor, Serializable {

private final AdvisedSupport advised;

public DynamicAdvisedInterceptor(AdvisedSupport advised) {
this.advised = advised;
}

@Override
@Nullable
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
Object oldProxy = null;
boolean setProxyContext = false;
Object target = null;
// 这里才是我的真实对象
TargetSource targetSource = this.advised.getTargetSource();
try {
if (this.advised.exposeProxy) {
// Make invocation available if necessary.
oldProxy = AopContext.setCurrentProxy(proxy);
setProxyContext = true;
}
// Get as late as possible to minimize the time we "own" the target, in case it comes from a pool...
target = targetSource.getTarget();
Class<?> targetClass = (target != null ? target.getClass() : null);
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
Object retVal;
// 然后拿到了我们上面装配的 Advisor 执行链条
if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) {
// 如果链条是空的并且方法是共有的话,直接执行方法!
Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
retVal = methodProxy.invoke(target, argsToUse);
}
else {
// 使用 CglibMethodInvocation 对象执行我们的方法
retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();
}
// 如果方法有返回值,转换为指定的类型.
retVal = processReturnType(proxy, target, method, retVal);
return retVal;
}
finally {
if (target != null && !targetSource.isStatic()) {
targetSource.releaseTarget(target);
}
if (setProxyContext) {
// Restore old proxy.
AopContext.setCurrentProxy(oldProxy);
}
}
}
// 省略其他代码
}

OK,接下来看看 CglibMethodInvocation#proceed() 做了什么事:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
CglibMethodInvocation#proceed:
@Override
@Nullable
public Object proceed() throws Throwable {
try {
return super.proceed();
}
catch (RuntimeException ex) {
throw ex;
}
catch (Exception ex) {
if (ReflectionUtils.declaresException(getMethod(), ex.getClass())) {
throw ex;
}
else {
throw new UndeclaredThrowableException(ex);
}
}
}
ReflectiveMethodInvocation#proceed:
@Override
@Nullable
public Object proceed() throws Throwable {
// 当在MethodAop中调用 Object proceed = joinPoint.proceed(); 的时候,此处会直接调用切入点的那个方法,就是当前的拦截器index === 最后一个的时候,是直接调用目标方法。
if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
return invokeJoinpoint();
}

Object interceptorOrInterceptionAdvice =
this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
// Evaluate dynamic method matcher here: static part will already have
// been evaluated and found to match.
InterceptorAndDynamicMethodMatcher dm =
(InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice;
Class<?> targetClass = (this.targetClass != null ? this.targetClass : this.method.getDeclaringClass());
if (dm.methodMatcher.matches(this.method, targetClass, this.arguments)) {
return dm.interceptor.invoke(this);
}
else {
// Dynamic matching failed.
// Skip this interceptor and invoke the next in the chain.
return proceed();
}
}
else {
// It's an interceptor, so we just invoke it: The pointcut will have
// been evaluated statically before this object was constructed.
return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
}
}

此时,第一个切入类是 SpringExposeInvocationInterceptor,直接走到下面的 return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public Object invoke(MethodInvocation mi) throws Throwable {
// 保存一个老的调用链
MethodInvocation oldInvocation = invocation.get();
// 当前调用链给了新的
invocation.set(mi);
try {
// 调用下一个拦截器
return mi.proceed();
}
finally {
// 恢复回去
invocation.set(oldInvocation);
}
}

重新进来还是调用 ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this) 这段代码: 此时进来的是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
AspectJAroundAdvice#invoke:
public Object invoke(MethodInvocation mi) throws Throwable {
if (!(mi instanceof ProxyMethodInvocation)) {
throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + mi);
}
ProxyMethodInvocation pmi = (ProxyMethodInvocation) mi;
// 这个参数就是我们写在 around 方法参数中的那个
// 此时我们的方法已经被织入在这个类的方法里边去
// 所以只要执行我们的 around 方法就是变相执行我们真实对象的方法了。
ProceedingJoinPoint pjp = lazyGetProceedingJoinPoint(pmi);
JoinPointMatch jpm = getJoinPointMatch(pmi);
return invokeAdviceMethod(pjp, jpm, null, null);
}
// As above, but in this case we are given the join point.
protected Object invokeAdviceMethod(JoinPoint jp, @Nullable JoinPointMatch jpMatch,
@Nullable Object returnValue, @Nullable Throwable t) throws Throwable {

return invokeAdviceMethodWithGivenArgs(argBinding(jp, jpMatch, returnValue, t));
}

protected Object invokeAdviceMethodWithGivenArgs(Object[] args) throws Throwable {
Object[] actualArgs = args;
if (this.aspectJAdviceMethod.getParameterCount() == 0) {
actualArgs = null;
}
try {
ReflectionUtils.makeAccessible(this.aspectJAdviceMethod);
// 使用反射直接调用advisor中的方法
return this.aspectJAdviceMethod.invoke(this.aspectInstanceFactory.getAspectInstance(), actualArgs);
}
catch (IllegalArgumentException ex) {
throw new AopInvocationException("Mismatch on arguments to advice method [" +
this.aspectJAdviceMethod + "]; pointcut expression [" +
this.pointcut.getPointcutExpression() + "]", ex);
}
catch (InvocationTargetException ex) {
throw ex.getTargetException();
}
}

jdk动态代理

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public Object getProxy(@Nullable ClassLoader classLoader) {
if (logger.isTraceEnabled()) {
logger.trace("Creating JDK dynamic proxy: " + this.advised.getTargetSource());
}
// 同样获取到所有的接口
// 这次 Spring 混入了三个:SpringProxy、Advised 和 DecoratingProxy
Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true);
// 获取接口中定义的 equals 方法
findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);
// 通过直接调用 jdk 的 Proxy 创建代理对象
return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
}

调用的时候则是使用 jdk 的 invoke 方式进行调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
@Override
@Nullable
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object oldProxy = null;
boolean setProxyContext = false;

TargetSource targetSource = this.advised.targetSource;
Object target = null;

try {
if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) {
// The target does not implement the equals(Object) method itself.
return equals(args[0]);
}
else if (!this.hashCodeDefined && AopUtils.isHashCodeMethod(method)) {
// The target does not implement the hashCode() method itself.
return hashCode();
}
else if (method.getDeclaringClass() == DecoratingProxy.class) {
// There is only getDecoratedClass() declared -> dispatch to proxy config.
return AopProxyUtils.ultimateTargetClass(this.advised);
}
else if (!this.advised.opaque && method.getDeclaringClass().isInterface() &&
method.getDeclaringClass().isAssignableFrom(Advised.class)) {
// Service invocations on ProxyConfig with the proxy config...
return AopUtils.invokeJoinpointUsingReflection(this.advised, method, args);
}

Object retVal;

// 上面均是特殊方法的代理形式,直接走到这里
if (this.advised.exposeProxy) {
// Make invocation available if necessary.
oldProxy = AopContext.setCurrentProxy(proxy);
setProxyContext = true;
}

// Get as late as possible to minimize the time we "own" the target,
// in case it comes from a pool.
target = targetSource.getTarget();
Class<?> targetClass = (target != null ? target.getClass() : null);

// 获取AOP拦截链
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);

// Check whether we have any advice. If we don't, we can fallback on direct
// reflective invocation of the target, and avoid creating a MethodInvocation.
if (chain.isEmpty()) {
// We can skip creating a MethodInvocation: just invoke the target directly
// Note that the final invoker must be an InvokerInterceptor so we know it does
// nothing but a reflective operation on the target, and no hot swapping or fancy proxying.
Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
}
else {
// 这一步就跟上面一样了
MethodInvocation invocation =
new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
// Proceed to the joinpoint through the interceptor chain.
retVal = invocation.proceed();
}

// Massage return value if necessary.
Class<?> returnType = method.getReturnType();
if (retVal != null && retVal == target &&
returnType != Object.class && returnType.isInstance(proxy) &&
!RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) {
// Special case: it returned "this" and the return type of the method
// is type-compatible. Note that we can't help if the target sets
// a reference to itself in another returned object.
retVal = proxy;
}
else if (retVal == null && returnType != Void.TYPE && returnType.isPrimitive()) {
throw new AopInvocationException(
"Null return value from advice does not match primitive return type for: " + method);
}
return retVal;
}
finally {
if (target != null && !targetSource.isStatic()) {
// Must have come from TargetSource.
targetSource.releaseTarget(target);
}
if (setProxyContext) {
// Restore old proxy.
AopContext.setCurrentProxy(oldProxy);
}
}
}

AOP中this的问题

这个问题是我印象最深的一个问题,曾经使用了 spring-cache,预想着通过注解,就可以解决缓存的问题,实在是一件很有感觉的事情,然而,在清理的时候,并不是这么简单…经常遇到我明明注解已经写了,但是就是没有给我清理的问题。后面就直接没有使用了。

1
2
3
4
5
6
7
8
9
10
11
12
@CacheEvict(value="userCache",key="#u.getUID()")
pubic void updateUser(User u) {
// ....逻辑
//调用其他方法让其重新缓存
userOfUID(u.getUID);
}

@Cacheable(value = "userCache",key="#result.getUID()")
pubic User userOfUID(String uid) {
userDao.userOfUID(uid);
}

然而走完 updateUser 缓存直接没了,并没有帮我刷新。 这个确实是 AOP 中模糊了 this 指向的问题。 我们通过刚刚的例子可以看到,其实 AOP 生效的时机是你从 BeanFactory 中拿到 Bean 的时候,因为这个时候你拿到的就是一个代理对象,那么会执行 AOPAdvisor 中定义的方法。但是如果我们直接在我们的类中使用 this 的时候,其实指向的是我们自己的类实例,没有走 Spring 外部的逻辑。 那咋办呢… 重点就在于怎么拿到 Spring 的代理对象!参考资料 通过配置 exposeProxy ,然后通过 ((UserService) AopContext.currentProxy()).userOfUID(); 来调用。 配置: 注解:@EnableAspectJAutoProxy(proxyTargteClass = true, exposeProxy = true) xml<aop:aspectj-autoproxy expose-proxy="true"/> 那我就比较懒(sao)了…「好了 还是不要这么用吧…」

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class UserServiceImpl implements UserService {

@Lazy
@Autowired
private UserService userService;

@CacheEvict(value="userCache",key="#u.getUID()")
pubic void updateUser(User u) {
// ....逻辑
//调用其他方法让其重新缓存
userService.userOfUID(u.getUID);
}

@Cacheable(value = "userCache",key="#result.getUID()")
pubic User userOfUID(String uid) {
userDao.userOfUID(uid);
}

}

接下来进入看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* Create a proxy with the configured interceptors if the bean is
* identified as one to proxy by the subclass.
* @see #getAdvicesAndAdvisorsForBean
*/
@Override
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
if (bean != null) {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
// 提早曝光的代理引用,如果不是相同的,则调用 wrapIfNecessary 进行包装
if (this.earlyProxyReferences.remove(cacheKey) != bean) {
return wrapIfNecessary(bean, beanName, cacheKey);
}
}
return bean;
}
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
return bean;
}
// 这里通过 cacheKey 判断当前 Bean 是否之前记录不需要切入(使用 advisedBeans 存储)
if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
return bean;
}
// 1. 如果是 Advice/Pointcut 等 AOP 相关的类则跳过;
// 2. BeanName以.ORIGINAL结尾的话,不做代理,如:com.mypackage.MyClass.ORIGINAL
if (isInfrastructureClass(bean.getClass()) shouldSkip(bean.getClass(), beanName)) {
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}

// 在这里开始获取 Advisor
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
if (specificInterceptors != DO_NOT_PROXY) {
this.advisedBeans.put(cacheKey, Boolean.TRUE);
// 开始创建代理对象
Object proxy = createProxy(
bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
this.proxyTypes.put(cacheKey, proxy.getClass());
return proxy;
}

this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}

获取advisor

如果暂时对如何获取 advisor 没兴趣的话,可以先跳过,后面再回来看

1
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);

这句话是用来获取 advisor 的,并且缓存起来项目中配置的 advisor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Override
@Nullable
protected Object[] getAdvicesAndAdvisorsForBean(
Class<?> beanClass, String beanName, @Nullable TargetSource targetSource) {

List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName);
if (advisors.isEmpty()) {
return DO_NOT_PROXY;
}
return advisors.toArray();
}

protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
List<Advisor> candidateAdvisors = findCandidateAdvisors();
List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
extendAdvisors(eligibleAdvisors);
if (!eligibleAdvisors.isEmpty()) {
eligibleAdvisors = sortAdvisors(eligibleAdvisors);
}
return eligibleAdvisors;
}

List<Advisor> candidateAdvisors = findCandidateAdvisors(); 这句话使用来获取已经缓存起来了的所有 Advisors。因为在当前我们已经无法进入到怎么解析并缓存的问题上来了(因为已经缓存起来了),所以我们需要退回到前面的步骤中来。

解析Advisor

然而什么时候做的解析和缓存,我们刚刚在上一节中说了 initializeBean 会在创建所有 Bean 的时候调用所有 beanPostProcessor 的前置处理器。那我们来看看 AnnotationAwareAspectJAutoProxyCreator 的前置处理器做了什么,并且第一次调用该方法是什么时候:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
@Override
public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) {
Object cacheKey = getCacheKey(beanClass, beanName);

// 这里在前置的时候就开始记录哪些 Bean 需要织入,哪些不需要了
if (!StringUtils.hasLength(beanName) !this.targetSourcedBeans.contains(beanName)) {
if (this.advisedBeans.containsKey(cacheKey)) {
return null;
}
// 初始化第一个 Bean(在我这里是我那个配置类的初始化)的时候就开始调用 shouldSkip
if (isInfrastructureClass(beanClass) shouldSkip(beanClass, beanName)) {
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return null;
}
}

// Create proxy here if we have a custom TargetSource.
// Suppresses unnecessary default instantiation of the target bean:
// The TargetSource will handle target instances in a custom fashion.
TargetSource targetSource = getCustomTargetSource(beanClass, beanName);
if (targetSource != null) {
if (StringUtils.hasLength(beanName)) {
this.targetSourcedBeans.add(beanName);
}
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(beanClass, beanName, targetSource);
Object proxy = createProxy(beanClass, beanName, specificInterceptors, targetSource);
this.proxyTypes.put(cacheKey, proxy.getClass());
return proxy;
}

return null;
}
@Override
protected boolean shouldSkip(Class<?> beanClass, String beanName) {
// 获取当前 BeanFactory 中所有的 advisor 对象,并且做缓存
List<Advisor> candidateAdvisors = findCandidateAdvisors();
for (Advisor advisor : candidateAdvisors) {
if (advisor instanceof AspectJPointcutAdvisor &&
((AspectJPointcutAdvisor) advisor).getAspectName().equals(beanName)) {
return true;
}
}
return super.shouldSkip(beanClass, beanName);
}

protected List<Advisor> findCandidateAdvisors() {
// 先调用下面那个在父类的方法
List<Advisor> advisors = super.findCandidateAdvisors();
// 开始构建存在 BeanFactory 中的 Advisor
if (this.aspectJAdvisorsBuilder != null) {
advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors());
}
return advisors;
}
protected List<Advisor> findCandidateAdvisors() {
Assert.state(this.advisorRetrievalHelper != null, "No BeanFactoryAdvisorRetrievalHelper available");
// 通过一个 advisorRetrievalHelper 来获取所有的 advisor Bean
return this.advisorRetrievalHelper.findAdvisorBeans();
}

advisorRetrievalHelper 是配置 auto-proxy 的时候,会被使用的一个类,他在 AnnotationAwareAspectJAutoProxyCreator 中传入一个 ConfigurableListableBeanFactory 作为参数调用构造器初始化。 所以,advisorRetrievalHelper 可以在当前容器中,查找所有的 BeanDefinition,并且取出是 AdvisorBean。 接下来来看看这个方法做了什么事情:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* Find all eligible Advisor beans in the current bean factory,
* ignoring FactoryBeans and excluding beans that are currently in creation.
* @return the list of {@link org.springframework.aop.Advisor} beans
* @see #isEligibleBean
*/
public List<Advisor> findAdvisorBeans() {
// Determine list of advisor bean names, if not cached already.
String[] advisorNames = this.cachedAdvisorBeanNames;
if (advisorNames == null) {
// Do not initialize FactoryBeans here: We need to leave all regular beans
// uninitialized to let the auto-proxy creator apply to them!
advisorNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
this.beanFactory, Advisor.class, true, false);
this.cachedAdvisorBeanNames = advisorNames;
}
// 第一次调用的时候,还没找到,所以直接就在这里被返回出去
if (advisorNames.length == 0) {
return new ArrayList<>();
}

List<Advisor> advisors = new ArrayList<>();
for (String name : advisorNames) {
if (isEligibleBean(name)) {
if (this.beanFactory.isCurrentlyInCreation(name)) {
if (logger.isTraceEnabled()) {
logger.trace("Skipping currently created advisor '" + name + "'");
}
}
else {
try {
advisors.add(this.beanFactory.getBean(name, Advisor.class));
}
catch (BeanCreationException ex) {
.....
continue;
}
}
throw ex;
}
}
}
}
return advisors;
}

OK,既然返回了一个空的集合,那么就来看看,后面做了什么事情了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
protected List<Advisor> findCandidateAdvisors() {
// 先调用下面那个在父类的方法
List<Advisor> advisors = super.findCandidateAdvisors();
// 开始构建存在 BeanFactory 中的 Advisor
if (this.aspectJAdvisorsBuilder != null) {
advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors());
}
return advisors;
}
public List<Advisor> buildAspectJAdvisors() {
List<String> aspectNames = this.aspectBeanNames;

if (aspectNames == null) {
synchronized (this) {
// 因为 aspectBeanNames 现在是 NULL,所以进入开始初始化
aspectNames = this.aspectBeanNames;
if (aspectNames == null) {
List<Advisor> advisors = new ArrayList<>();
aspectNames = new ArrayList<>();
// 获取 BeanFactory 中所有的 beanNames
String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
this.beanFactory, Object.class, true, false);
// 二话不说就来了个循环
for (String beanName : beanNames) {
// 是否是个有资格的类?然而方法都是直接返回 true...也就是这一步永远不会进去
// 感觉这个方法是个有故事的方法,先不看
if (!isEligibleBean(beanName)) {
continue;
}
// 这一步必须使用 getType 调用 getBean 可能会导致错误(那个Bean需要被织入但是太早初始化的
// 话会导致没有织入成功)
Class<?> beanType = this.beanFactory.getType(beanName);
if (beanType == null) {
continue;
}
// 这一步会判断类有没有被 @Aspect 修饰
if (this.advisorFactory.isAspect(beanType)) {
aspectNames.add(beanName);
AspectMetadata amd = new AspectMetadata(beanType, beanName);
// 根据 Advisor 是否可以为单例(这个是AOP定义的单例)来缓存 AOP 信息以便后面可以使用
if (amd.getAjType().getPerClause().getKind() == PerClauseKind.SINGLETON) {
// 构建 Advisor 工厂
MetadataAwareAspectInstanceFactory factory =
new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName);
List<Advisor> classAdvisors = this.advisorFactory.getAdvisors(factory);
// 如果是单例(BeanFactory的单例)的 Bean,则直接缓存 Advisor 对象
if (this.beanFactory.isSingleton(beanName)) {
this.advisorsCache.put(beanName, classAdvisors);
}
else {
this.aspectFactoryCache.put(beanName, factory);
}
advisors.addAll(classAdvisors);
}
else {
// 每个目标对象或者每个对象都去织入.
if (this.beanFactory.isSingleton(beanName)) {
throw new IllegalArgumentException("Bean with name '" + beanName +
"' is a singleton, but aspect instantiation model is not singleton");
}
MetadataAwareAspectInstanceFactory factory =
new PrototypeAspectInstanceFactory(this.beanFactory, beanName);
this.aspectFactoryCache.put(beanName, factory);
advisors.addAll(this.advisorFactory.getAdvisors(factory));
}
}
}
this.aspectBeanNames = aspectNames;
return advisors;
}
}
}

// 下一次再调用该方法的时候,即可直接通过缓存中取出 Advisor 使用
if (aspectNames.isEmpty()) {
return Collections.emptyList();
}
List<Advisor> advisors = new ArrayList<>();
for (String aspectName : aspectNames) {
List<Advisor> cachedAdvisors = this.advisorsCache.get(aspectName);
if (cachedAdvisors != null) {
advisors.addAll(cachedAdvisors);
}
else {
MetadataAwareAspectInstanceFactory factory = this.aspectFactoryCache.get(aspectName);
advisors.addAll(this.advisorFactory.getAdvisors(factory));
}
}
return advisors;
}

获取所有的Advisor

刚刚我们来到这里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
@Nullable
protected Object[] getAdvicesAndAdvisorsForBean(
Class<?> beanClass, String beanName, @Nullable TargetSource targetSource) {

List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName);
if (advisors.isEmpty()) {
return DO_NOT_PROXY;
}
return advisors.toArray();
}

protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
List<Advisor> candidateAdvisors = findCandidateAdvisors();// <-----
// 获取装配当前 Bean 的 Advisor
// 这一步其实就是获取 @Pointcut 中配置的装配规则来匹配所有的方法,我偷懒,先过
List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
// 扩展Advisor
extendAdvisors(eligibleAdvisors);
if (!eligibleAdvisors.isEmpty()) {
eligibleAdvisors = sortAdvisors(eligibleAdvisors);
}
return eligibleAdvisors;
}

现在在这个方法内部,已经获取我们自己配置的 MethodAop 对象了,但是下面还有个扩展,看看扩展了什么东西:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* Adds an {@link ExposeInvocationInterceptor} to the beginning of the advice chain.
* These additional advices are needed when using AspectJ expression pointcuts
* and when using AspectJ-style advice.
*/
@Override
protected void extendAdvisors(List<Advisor> candidateAdvisors) {
AspectJProxyUtils.makeAdvisorChainAspectJCapableIfNecessary(candidateAdvisors);
}

public static boolean makeAdvisorChainAspectJCapableIfNecessary(List<Advisor> advisors) {
// Don't add advisors to an empty list; may indicate that proxying is just not required
if (!advisors.isEmpty()) {
boolean foundAspectJAdvice = false;
for (Advisor advisor : advisors) {
// 如果是 AspectJAdvice 则不添加
if (isAspectJAdvice(advisor)) {
foundAspectJAdvice = true;
break;
}
}
// 我们织入的方法中,都会添加一个 ExposeInvocationInterceptor 实例
if (foundAspectJAdvice && !advisors.contains(ExposeInvocationInterceptor.ADVISOR)) {
advisors.add(0, ExposeInvocationInterceptor.ADVISOR);
return true;
}
}
return false;
}

这里有两个概念:

  1. AspectJAdvice:通过编译器织入 AOP 代码的一个框架;

  2. ExposeInvocationInterceptor:这是一个运行期织入时可以随时获取整个调用链的内置的 AOP 拦截器,如果在使用过程中需要获取调用链的话,可以通过 ExposeInvocationInterceptor.currentInvocation 来获取。SpringAOP 在织入的时候默认在第一个织入这个实例。

好了,走完全部,获取所有 advisors 的过程终于完成。

织入运行期对象

回到 postProcessAfterInitialization -> wrapIfNecessary 方法中来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
return bean;
}
// 这里通过 cacheKey 判断当前 Bean 是否之前记录不需要切入(使用 advisedBeans 存储)
if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
return bean;
}
// 1. 如果是 Advice/Pointcut 等 AOP 相关的类则跳过;
// 2. BeanName以.ORIGINAL结尾的话,不做代理,如:com.mypackage.MyClass.ORIGINAL
if (isInfrastructureClass(bean.getClass()) shouldSkip(bean.getClass(), beanName)) {
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}

// 在这里开始获取 Advisor
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
if (specificInterceptors != DO_NOT_PROXY) {
this.advisedBeans.put(cacheKey, Boolean.TRUE);
// 开始创建代理对象
Object proxy = createProxy(
bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
this.proxyTypes.put(cacheKey, proxy.getClass());
return proxy;
}

this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}

接下来就是开始创建代理对象了。创建对象有两种方式:cglibjdk代理,使用哪种方式取决于当前那个类有没有接口实现,如果是一个普通的类(没有实现接口的方式),则使用前者,否则用后者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
@Nullable Object[] specificInterceptors, TargetSource targetSource) {

if (this.beanFactory instanceof ConfigurableListableBeanFactory) {
// 记录原始数据
AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass);
}

// 创建 ProxyFactory 用于创建代理
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.copyFrom(this);

if (!proxyFactory.isProxyTargetClass()) {
if (shouldProxyTargetClass(beanClass, beanName)) {
// 直接代理类实例(通过BeanDefinition配置的属性来判断)
proxyFactory.setProxyTargetClass(true);
}
else {
// 计算所有的接口设置到工厂,如果没有接口,同上直接代理实例
evaluateProxyInterfaces(beanClass, proxyFactory);
}
}

// 通过上一步获取的所有 advisors,过滤获取匹配的 advisors
Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
proxyFactory.addAdvisors(advisors);
proxyFactory.setTargetSource(targetSource);
customizeProxyFactory(proxyFactory);

proxyFactory.setFrozen(this.freezeProxy);
if (advisorsPreFiltered()) {
proxyFactory.setPreFiltered(true);
}

// 开始构建
return proxyFactory.getProxy(getProxyClassLoader());
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public Object getProxy(@Nullable ClassLoader classLoader) {
// 调用 DefaultAopProxyFactory 开始构建代理
return createAopProxy().getProxy(classLoader);
}
protected final synchronized AopProxy createAopProxy() {
if (!this.active) {
activate();
}
// getAopProxyFactory返回DefaultAopProxyFactory实例
return getAopProxyFactory().createAopProxy(this);
}
DefaultAopProxyFactory#createAopProxy
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
if (config.isOptimize() config.isProxyTargetClass() hasNoUserSuppliedProxyInterfaces(config)) {
Class<?> targetClass = config.getTargetClass();
if (targetClass == null) {
throw new AopConfigException("TargetSource cannot determine target class: " +
"Either an interface or a target is required for proxy creation.");
}
if (targetClass.isInterface() Proxy.isProxyClass(targetClass)) {
return new JdkDynamicAopProxy(config);
}
// 如果没有接口,来到这
return new ObjenesisCglibAopProxy(config);
}
else {
return new JdkDynamicAopProxy(config);
}
}

createAopProxy 之后就是调用各自的实现 getProxy(classLoader)

Cglib

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
@Override
public Object getProxy(@Nullable ClassLoader classLoader) {
if (logger.isTraceEnabled()) {
logger.trace("Creating CGLIB proxy: " + this.advised.getTargetSource());
}

try {
// 获取对象的实际类型
Class<?> rootClass = this.advised.getTargetClass();
Assert.state(rootClass != null, "Target class must be available for creating a CGLIB proxy");

// 如果已经是一个 cglib 对象的时候,获取其父级(实际类型)
Class<?> proxySuperClass = rootClass;
if (rootClass.getName().contains(ClassUtils.CGLIB_CLASS_SEPARATOR)) {
proxySuperClass = rootClass.getSuperclass();
Class<?>[] additionalInterfaces = rootClass.getInterfaces();
for (Class<?> additionalInterface : additionalInterfaces) {
this.advised.addInterface(additionalInterface);
}
}

// Validate the class, writing log messages as necessary.
validateClassIfNecessary(proxySuperClass, classLoader);

// 开始生成 cglib 对象
Enhancer enhancer = createEnhancer();
if (classLoader != null) {
enhancer.setClassLoader(classLoader);
if (classLoader instanceof SmartClassLoader &&
((SmartClassLoader) classLoader).isClassReloadable(proxySuperClass)) {
enhancer.setUseCache(false);
}
}
enhancer.setSuperclass(proxySuperClass);
// 这里Spring混合了自己的两个接口:SpringProxy和Advised
enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised));
// 类加上 BySpringCGLIB 的标志
enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
enhancer.setStrategy(new ClassLoaderAwareGeneratorStrategy(classLoader));

Callback[] callbacks = getCallbacks(rootClass);
Class<?>[] types = new Class<?>[callbacks.length];
for (int x = 0; x < types.length; x++) {
types[x] = callbacks[x].getClass();
}
// fixedInterceptorMap only populated at this point, after getCallbacks call above
enhancer.setCallbackFilter(new ProxyCallbackFilter(
this.advised.getConfigurationOnlyCopy(), this.fixedInterceptorMap, this.fixedInterceptorOffset));
enhancer.setCallbackTypes(types);

// 初始化代理对象
return createProxyClassAndInstance(enhancer, callbacks);
}
catch (CodeGenerationException IllegalArgumentException ex) {
throw new AopConfigException("Could not generate CGLIB subclass of " + this.advised.getTargetClass() +
": Common causes of this problem include using a final class or a non-visible class",
ex);
}
catch (Throwable ex) {
// TargetSource.getTarget() failed
throw new AopConfigException("Unexpected AOP exception", ex);
}
}

OK,那让我们来看看调用的时候是什么情况: 现在我的对象已经变成 NoInterfaceService$$EnhancerBySpringCGLIB 类型了,那我在调用的时候不可否认的会进入到真实的对象里边去,此时我调用一下 test() 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
private static class DynamicAdvisedInterceptor implements MethodInterceptor, Serializable {

private final AdvisedSupport advised;

public DynamicAdvisedInterceptor(AdvisedSupport advised) {
this.advised = advised;
}

@Override
@Nullable
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
Object oldProxy = null;
boolean setProxyContext = false;
Object target = null;
// 这里才是我的真实对象
TargetSource targetSource = this.advised.getTargetSource();
try {
if (this.advised.exposeProxy) {
// Make invocation available if necessary.
oldProxy = AopContext.setCurrentProxy(proxy);
setProxyContext = true;
}
// Get as late as possible to minimize the time we "own" the target, in case it comes from a pool...
target = targetSource.getTarget();
Class<?> targetClass = (target != null ? target.getClass() : null);
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
Object retVal;
// 然后拿到了我们上面装配的 Advisor 执行链条
if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) {
// 如果链条是空的并且方法是共有的话,直接执行方法!
Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
retVal = methodProxy.invoke(target, argsToUse);
}
else {
// 使用 CglibMethodInvocation 对象执行我们的方法
retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();
}
// 如果方法有返回值,转换为指定的类型.
retVal = processReturnType(proxy, target, method, retVal);
return retVal;
}
finally {
if (target != null && !targetSource.isStatic()) {
targetSource.releaseTarget(target);
}
if (setProxyContext) {
// Restore old proxy.
AopContext.setCurrentProxy(oldProxy);
}
}
}
// 省略其他代码
}

OK,接下来看看 CglibMethodInvocation#proceed() 做了什么事:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
CglibMethodInvocation#proceed:
@Override
@Nullable
public Object proceed() throws Throwable {
try {
return super.proceed();
}
catch (RuntimeException ex) {
throw ex;
}
catch (Exception ex) {
if (ReflectionUtils.declaresException(getMethod(), ex.getClass())) {
throw ex;
}
else {
throw new UndeclaredThrowableException(ex);
}
}
}
ReflectiveMethodInvocation#proceed:
@Override
@Nullable
public Object proceed() throws Throwable {
// 当在MethodAop中调用 Object proceed = joinPoint.proceed(); 的时候,此处会直接调用切入点的那个方法,就是当前的拦截器index === 最后一个的时候,是直接调用目标方法。
if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
return invokeJoinpoint();
}

Object interceptorOrInterceptionAdvice =
this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
// Evaluate dynamic method matcher here: static part will already have
// been evaluated and found to match.
InterceptorAndDynamicMethodMatcher dm =
(InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice;
Class<?> targetClass = (this.targetClass != null ? this.targetClass : this.method.getDeclaringClass());
if (dm.methodMatcher.matches(this.method, targetClass, this.arguments)) {
return dm.interceptor.invoke(this);
}
else {
// Dynamic matching failed.
// Skip this interceptor and invoke the next in the chain.
return proceed();
}
}
else {
// It's an interceptor, so we just invoke it: The pointcut will have
// been evaluated statically before this object was constructed.
return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
}
}

此时,第一个切入类是 SpringExposeInvocationInterceptor,直接走到下面的 return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public Object invoke(MethodInvocation mi) throws Throwable {
// 保存一个老的调用链
MethodInvocation oldInvocation = invocation.get();
// 当前调用链给了新的
invocation.set(mi);
try {
// 调用下一个拦截器
return mi.proceed();
}
finally {
// 恢复回去
invocation.set(oldInvocation);
}
}

重新进来还是调用 ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this) 这段代码: 此时进来的是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
AspectJAroundAdvice#invoke:
public Object invoke(MethodInvocation mi) throws Throwable {
if (!(mi instanceof ProxyMethodInvocation)) {
throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + mi);
}
ProxyMethodInvocation pmi = (ProxyMethodInvocation) mi;
// 这个参数就是我们写在 around 方法参数中的那个
// 此时我们的方法已经被织入在这个类的方法里边去
// 所以只要执行我们的 around 方法就是变相执行我们真实对象的方法了。
ProceedingJoinPoint pjp = lazyGetProceedingJoinPoint(pmi);
JoinPointMatch jpm = getJoinPointMatch(pmi);
return invokeAdviceMethod(pjp, jpm, null, null);
}
// As above, but in this case we are given the join point.
protected Object invokeAdviceMethod(JoinPoint jp, @Nullable JoinPointMatch jpMatch,
@Nullable Object returnValue, @Nullable Throwable t) throws Throwable {

return invokeAdviceMethodWithGivenArgs(argBinding(jp, jpMatch, returnValue, t));
}

protected Object invokeAdviceMethodWithGivenArgs(Object[] args) throws Throwable {
Object[] actualArgs = args;
if (this.aspectJAdviceMethod.getParameterCount() == 0) {
actualArgs = null;
}
try {
ReflectionUtils.makeAccessible(this.aspectJAdviceMethod);
// 使用反射直接调用advisor中的方法
return this.aspectJAdviceMethod.invoke(this.aspectInstanceFactory.getAspectInstance(), actualArgs);
}
catch (IllegalArgumentException ex) {
throw new AopInvocationException("Mismatch on arguments to advice method [" +
this.aspectJAdviceMethod + "]; pointcut expression [" +
this.pointcut.getPointcutExpression() + "]", ex);
}
catch (InvocationTargetException ex) {
throw ex.getTargetException();
}
}

jdk动态代理

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public Object getProxy(@Nullable ClassLoader classLoader) {
if (logger.isTraceEnabled()) {
logger.trace("Creating JDK dynamic proxy: " + this.advised.getTargetSource());
}
// 同样获取到所有的接口
// 这次 Spring 混入了三个:SpringProxy、Advised 和 DecoratingProxy
Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true);
// 获取接口中定义的 equals 方法
findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);
// 通过直接调用 jdk 的 Proxy 创建代理对象
return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
}

调用的时候则是使用 jdk 的 invoke 方式进行调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
@Override
@Nullable
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object oldProxy = null;
boolean setProxyContext = false;

TargetSource targetSource = this.advised.targetSource;
Object target = null;

try {
if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) {
// The target does not implement the equals(Object) method itself.
return equals(args[0]);
}
else if (!this.hashCodeDefined && AopUtils.isHashCodeMethod(method)) {
// The target does not implement the hashCode() method itself.
return hashCode();
}
else if (method.getDeclaringClass() == DecoratingProxy.class) {
// There is only getDecoratedClass() declared -> dispatch to proxy config.
return AopProxyUtils.ultimateTargetClass(this.advised);
}
else if (!this.advised.opaque && method.getDeclaringClass().isInterface() &&
method.getDeclaringClass().isAssignableFrom(Advised.class)) {
// Service invocations on ProxyConfig with the proxy config...
return AopUtils.invokeJoinpointUsingReflection(this.advised, method, args);
}

Object retVal;

// 上面均是特殊方法的代理形式,直接走到这里
if (this.advised.exposeProxy) {
// Make invocation available if necessary.
oldProxy = AopContext.setCurrentProxy(proxy);
setProxyContext = true;
}

// Get as late as possible to minimize the time we "own" the target,
// in case it comes from a pool.
target = targetSource.getTarget();
Class<?> targetClass = (target != null ? target.getClass() : null);

// 获取AOP拦截链
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);

// Check whether we have any advice. If we don't, we can fallback on direct
// reflective invocation of the target, and avoid creating a MethodInvocation.
if (chain.isEmpty()) {
// We can skip creating a MethodInvocation: just invoke the target directly
// Note that the final invoker must be an InvokerInterceptor so we know it does
// nothing but a reflective operation on the target, and no hot swapping or fancy proxying.
Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
}
else {
// 这一步就跟上面一样了
MethodInvocation invocation =
new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
// Proceed to the joinpoint through the interceptor chain.
retVal = invocation.proceed();
}

// Massage return value if necessary.
Class<?> returnType = method.getReturnType();
if (retVal != null && retVal == target &&
returnType != Object.class && returnType.isInstance(proxy) &&
!RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) {
// Special case: it returned "this" and the return type of the method
// is type-compatible. Note that we can't help if the target sets
// a reference to itself in another returned object.
retVal = proxy;
}
else if (retVal == null && returnType != Void.TYPE && returnType.isPrimitive()) {
throw new AopInvocationException(
"Null return value from advice does not match primitive return type for: " + method);
}
return retVal;
}
finally {
if (target != null && !targetSource.isStatic()) {
// Must have come from TargetSource.
targetSource.releaseTarget(target);
}
if (setProxyContext) {
// Restore old proxy.
AopContext.setCurrentProxy(oldProxy);
}
}
}

AOP中this的问题

这个问题是我印象最深的一个问题,曾经使用了 spring-cache,预想着通过注解,就可以解决缓存的问题,实在是一件很有感觉的事情,然而,在清理的时候,并不是这么简单…经常遇到我明明注解已经写了,但是就是没有给我清理的问题。后面就直接没有使用了。

1
2
3
4
5
6
7
8
9
10
11
12
@CacheEvict(value="userCache",key="#u.getUID()")
pubic void updateUser(User u) {
// ....逻辑
//调用其他方法让其重新缓存
userOfUID(u.getUID);
}

@Cacheable(value = "userCache",key="#result.getUID()")
pubic User userOfUID(String uid) {
userDao.userOfUID(uid);
}

然而走完 updateUser 缓存直接没了,并没有帮我刷新。 这个确实是 AOP 中模糊了 this 指向的问题。 我们通过刚刚的例子可以看到,其实 AOP 生效的时机是你从 BeanFactory 中拿到 Bean 的时候,因为这个时候你拿到的就是一个代理对象,那么会执行 AOPAdvisor 中定义的方法。但是如果我们直接在我们的类中使用 this 的时候,其实指向的是我们自己的类实例,没有走 Spring 外部的逻辑。 那咋办呢… 重点就在于怎么拿到 Spring 的代理对象!参考资料 通过配置 exposeProxy ,然后通过 ((UserService) AopContext.currentProxy()).userOfUID(); 来调用。 配置: 注解:@EnableAspectJAutoProxy(proxyTargteClass = true, exposeProxy = true) xml<aop:aspectj-autoproxy expose-proxy="true"/> 那我就比较懒(sao)了…「好了 还是不要这么用吧…」

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class UserServiceImpl implements UserService {

@Lazy
@Autowired
private UserService userService;

@CacheEvict(value="userCache",key="#u.getUID()")
pubic void updateUser(User u) {
// ....逻辑
//调用其他方法让其重新缓存
userService.userOfUID(u.getUID);
}

@Cacheable(value = "userCache",key="#result.getUID()")
pubic User userOfUID(String uid) {
userDao.userOfUID(uid);
}

}

简述

回顾一下上一篇,使用的是一个简单的 BeanFactory 实现 XmlBeanFactory。这个容器已经是一个过时的容器了,因为他并不能实现除了注册 BeanDefinition 之外的事情(比如 i18n 访问资源(骚一点的话可以加载网络 Spring 配置文件) 应用事件)。所以,就有了 spring-context 的出现。 我们现在知道了,Spring 把载入、解析、注册解耦成不同的模块,所以我们现在可以大概的知道:

  1. 载入:我们可以使用不同的方式载入配置文件,然后配置解析方式,生成 BeanDefinition 注册到底层的 Bean 容器中;
  2. 解析:不同的配置文件解析方式有所不同,但是有个共同的目的就是解析成 BeanDefinition 注册到容器中去;
  3. 注册:这块 Spring 已经提供了 DefaultListableBeanFactory 这个成熟的实现,所以即使我们想通过其他配置文件来配置我们的 Spring 项目的话,不用慌,只要用它就好了(奥森!)

那么仔细想想,我们是不是可以实现由 JSON 格式配置文件来配置的 BeanFactory

一.一个可运行的DEMO

因为 Spring 现在支持 Java注解 来生成对象,所以我想在看 ApplicationContext 源码的时候,顺便把怎么从配置里面取出 Bean 的过程给看了,所以配置文件只配置了一个 Java配置类。

applicationContext.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
">

<bean class="cn.liweidan.confbean.MyBeanConfiguration"/>
<context:annotation-config/>
</beans>

MyBeanConfiguration.java

这个写法在 SpringBoot 横行霸道的时候应该不会陌生。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package cn.liweidan.confbean;

import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.context.annotation.Bean;

@Configurable
public class MyBeanConfiguration {

@Bean
public MyBean myBean() {
MyBean myBean = new MyBean();
myBean.setName("Weidan");
return myBean;
}

}

MyBean.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package cn.liweidan.confbean;

public class MyBean {

private String name;

public MyBean(String name) {
this.name = name;
}

public MyBean() {
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}

启动器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package cn.liweidan.confbean;

import org.junit.Assert;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class ConfigBeanTest {

@Test
public void testConfigBean() {
BeanFactory bf = new ClassPathXmlApplicationContext("applicationContext.xml");
MyBean bean = bf.getBean(MyBean.class);
Assert.assertNotNull(bean);
}

}

跑一下测试用例,恩!是绿色的感觉!(绿色?)

二.源码解析

其实我一直没有经验怎么写源码的解析,看得人才能容易看的明白,所以我就按照 debug 走的顺序来走了,在哪个类的哪个方法,都会放在三级标题上。

简述

回顾一下上一篇,使用的是一个简单的 BeanFactory 实现 XmlBeanFactory。这个容器已经是一个过时的容器了,因为他并不能实现除了注册 BeanDefinition 之外的事情(比如 i18n 访问资源(骚一点的话可以加载网络 Spring 配置文件) 应用事件)。所以,就有了 spring-context 的出现。 我们现在知道了,Spring 把载入、解析、注册解耦成不同的模块,所以我们现在可以大概的知道:

  1. 载入:我们可以使用不同的方式载入配置文件,然后配置解析方式,生成 BeanDefinition 注册到底层的 Bean 容器中;
  2. 解析:不同的配置文件解析方式有所不同,但是有个共同的目的就是解析成 BeanDefinition 注册到容器中去;
  3. 注册:这块 Spring 已经提供了 DefaultListableBeanFactory 这个成熟的实现,所以即使我们想通过其他配置文件来配置我们的 Spring 项目的话,不用慌,只要用它就好了(奥森!)

那么仔细想想,我们是不是可以实现由 JSON 格式配置文件来配置的 BeanFactory

一.一个可运行的DEMO

因为 Spring 现在支持 Java注解 来生成对象,所以我想在看 ApplicationContext 源码的时候,顺便把怎么从配置里面取出 Bean 的过程给看了,所以配置文件只配置了一个 Java配置类。

applicationContext.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
">

<bean class="cn.liweidan.confbean.MyBeanConfiguration"/>
<context:annotation-config/>
</beans>

MyBeanConfiguration.java

这个写法在 SpringBoot 横行霸道的时候应该不会陌生。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package cn.liweidan.confbean;

import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.context.annotation.Bean;

@Configurable
public class MyBeanConfiguration {

@Bean
public MyBean myBean() {
MyBean myBean = new MyBean();
myBean.setName("Weidan");
return myBean;
}

}

MyBean.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package cn.liweidan.confbean;

public class MyBean {

private String name;

public MyBean(String name) {
this.name = name;
}

public MyBean() {
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}

启动器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package cn.liweidan.confbean;

import org.junit.Assert;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class ConfigBeanTest {

@Test
public void testConfigBean() {
BeanFactory bf = new ClassPathXmlApplicationContext("applicationContext.xml");
MyBean bean = bf.getBean(MyBean.class);
Assert.assertNotNull(bean);
}

}

跑一下测试用例,恩!是绿色的感觉!(绿色?)

二.源码解析

其实我一直没有经验怎么写源码的解析,看得人才能容易看的明白,所以我就按照 debug 走的顺序来走了,在哪个类的哪个方法,都会放在三级标题上。

好了,我们已经可以看到,application 是一个 BeanFactory 但是他又没有去继承我们之前看到的 BeanFactory 的实现类。为啥,他并不是一个单纯的 BeanFactory 实现,而是聚合了 BeanFactory 实现类,来实现 BeanFactory 的所有功能,所以,它拥有 BeanFactory 接口的所有功能。(还记得那句话吗?接口表示实现类会什么(class has xxx),继承表示实现类是什么(class is xxx))

2.1 ClassPathXmlApplicationContext构造器

好,首先从这段代码开始:

1
BeanFactory bf = new ClassPathXmlApplicationContext("applicationContext.xml");

ClassPathXmlApplicationContext 其实是一个简单到不能再简单的类了,他的主要功能就是记录用户传递的 applicationContext.xml 的路径,是一个字符串数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ClassPathXmlApplicationContext extends AbstractXmlApplicationContext {

@Nullable
private Resource[] configResources;

/** 这是我们进入的时候调用的构造方法 */
public ClassPathXmlApplicationContext(String... configLocations) throws BeansException {
// 调用了下面的构造方法
this(configLocations, true, null);
}

public ClassPathXmlApplicationContext(
String[] configLocations, boolean refresh, @Nullable ApplicationContext parent)
throws BeansException {
// 这里是一个 Null,我就不进去看了,反正就是之前熟悉的
// 设置 parentBeanFactory
super(parent);
setConfigLocations(configLocations);
if (refresh) {
refresh();
}
}

}

OK,接下来我们进入这句话:setConfigLocations(configLocations);

2.2 AbstractRefreshableConfigApplicationContext#setConfigLocations

这句话的实现是在父级 AbstractRefreshableConfigApplicationContext 实现的,先简单了解下这句话是做什么用的,这句话是设置我 AbstractRefreshableConfigApplicationContext 的配置路径的属性,顺便通过环境的配置,把类似于 application-${someProp} 的配置,给还原成我们配置再环境的内容。 先看下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public abstract class AbstractRefreshableConfigApplicationContext extends AbstractRefreshableApplicationContext
implements BeanNameAware, InitializingBean {

public void setConfigLocations(@Nullable String... locations) {
if (locations != null) {
Assert.noNullElements(locations, "Config locations must not be null");
this.configLocations = new String[locations.length];
for (int i = 0; i < locations.length; i++) {
// 重点要跟着这一句走
this.configLocations[i] = resolvePath(locations[i]).trim();
}
}
else {
this.configLocations = null;
}
}

protected String resolvePath(String path) {
return getEnvironment().resolveRequiredPlaceholders(path);
}

/*
getEnvironment()这一句还是在父类 AbstractApplicationContext 实现的:
protected ConfigurableEnvironment createEnvironment() {
return new StandardEnvironment();
}
*/

}

简单的创建了一个 StandardEnvironment 对象,我想这是因为我在类似于 main 方法运行的缘故,如果是 web 环境有另外一种实现 StandardServletEnvironmentStandardServletEnvironment#resolveRequiredPlaceholders 代码则是把这件事情交给 core 里面的工具类 PropertyPlaceholderHelper#parseStringValue 来实现,由于这件事情不是我要看的重点,所以我先直接跳过去好了。 然后直接下一步,if (refresh) {...} 因为构造器直接写死传递了一个 true,所以现在需要直接进入 refresh() 方法来做。

2.3 AbstractApplicationContext#refresh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
prepareRefresh();

// Tell the subclass to refresh the internal bean factory.
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);

try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);

// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);

// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);

// Initialize message source for this context.
initMessageSource();

// Initialize event multicaster for this context.
initApplicationEventMulticaster();

// Initialize other special beans in specific context subclasses.
onRefresh();

// Check for listener beans and register them.
registerListeners();

// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);

// Last step: publish corresponding event.
finishRefresh();
}

catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}

// Destroy already created singletons to avoid dangling resources.
destroyBeans();

// Reset 'active' flag.
cancelRefresh(ex);

// Propagate exception to caller.
throw ex;
}

finally {
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
resetCommonCaches();
}
}
}

AbstractApplicationContext 看起来就是一个快要成型的 ApplicationContext 了,这里 refresh() 方法看起来就是调用一些其他方法来做启动的。

2.3.1 AbstractApplicationContext#prepareRefresh

那首先就是准备环境:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
protected void prepareRefresh() {
// Switch to active.
this.startupDate = System.currentTimeMillis();
// 设置非关闭状态
this.closed.set(false);
// 已激活状态
this.active.set(true);

if (logger.isDebugEnabled()) {
if (logger.isTraceEnabled()) {
logger.trace("Refreshing " + this);
}
else {
logger.debug("Refreshing " + getDisplayName());
}
}

// 这是一个空的方法,允许子类重写,做一些其他属性源的准备。
initPropertySources();

// 验证用户设置的必要的属性,如果这些属性确实则会抛出异常
// 目前来说我没有设置任何必要的属性,所以这一步是空的实现
getEnvironment().validateRequiredProperties();

// 下面代码都是初始化 applicationListeners 的容器
// 也就是 applicationContext 启动前我们注册的事件监听器
if (this.earlyApplicationListeners == null) {
this.earlyApplicationListeners = new LinkedHashSet<>(this.applicationListeners);
}
else {
// Reset local application listeners to pre-refresh state.
this.applicationListeners.clear();
this.applicationListeners.addAll(this.earlyApplicationListeners);
}

// Allow for the collection of early ApplicationEvents,
// to be published once the multicaster is available...
this.earlyApplicationEvents = new LinkedHashSet<>();
}

初始化我感觉没做什么特别的事情,也就是初始化一些必要的容器。 接下来就是大事情了:

1
2
// Tell the subclass to refresh the internal bean factory.
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

创建一个 BeanFactory

2.3.2 AbstractApplicationContext#obtainFreshBeanFactory

1
2
3
4
5
6
7
8
protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
refreshBeanFactory();
return getBeanFactory();
}

protected abstract void refreshBeanFactory() throws BeansException, IllegalStateException;

public abstract ConfigurableListableBeanFactory getBeanFactory() throws IllegalStateException;

好了,这就是一个模板方法的设计模式,调用了两个需要子类实现的方法。


接下来看看子类怎么创建的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Override
protected final void refreshBeanFactory() throws BeansException {
if (hasBeanFactory()) {
destroyBeans();
closeBeanFactory();
}
try {
DefaultListableBeanFactory beanFactory = createBeanFactory();
beanFactory.setSerializationId(getId());
customizeBeanFactory(beanFactory);
loadBeanDefinitions(beanFactory);
synchronized (this.beanFactoryMonitor) {
this.beanFactory = beanFactory;
}
}
catch (IOException ex) {
throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex);
}
}


@Override
public final ConfigurableListableBeanFactory getBeanFactory() {
synchronized (this.beanFactoryMonitor) {
if (this.beanFactory == null) {
throw new IllegalStateException("BeanFactory not initialized or already closed - " +
"call 'refresh' before accessing beans via the ApplicationContext");
}
return this.beanFactory;
}
}

OK,我们现在看到,首先检测当前是否有工厂实例存在,如果有,直接清理掉里面的数据。然后创建、配置、并且返回回去。在子类看来就只有做这几件事情。 那在当前类的 refreshBeanFactory 方法中,就是着重的看着,是怎么配置并且构建的就 ok 了。 首先看 DefaultListableBeanFactory beanFactory = createBeanFactory(); 这个:

1
2
3
protected DefaultListableBeanFactory createBeanFactory() {
return new DefaultListableBeanFactory(getInternalParentBeanFactory());
}

好吧,过! 然后开始设置 SerializationId 这个也不需要看。 下一个是 customizeBeanFactory(beanFactory);

1
2
3
4
5
6
7
8
protected void customizeBeanFactory(DefaultListableBeanFactory beanFactory) {
if (this.allowBeanDefinitionOverriding != null) {
beanFactory.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
}
if (this.allowCircularReferences != null) {
beanFactory.setAllowCircularReferences(this.allowCircularReferences);
}
}

这是两个自定义的属性,一个是是否允许覆盖 Bean 的定义,一个是是否允许相互依赖,但是这两个 this. 开头的都是 null,如果子类需要修改这两个值的话,则需要在子类调用相对应的 setter。因为没有调用,所以均使用 DefaultListableBeanFactory 默认设置的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory
implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable {

/** Whether to allow re-registration of a different definition with the same name. */
private boolean allowBeanDefinitionOverriding = true;

}

public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory
implements AutowireCapableBeanFactory {

/** Whether to automatically try to resolve circular references between beans. */
private boolean allowCircularReferences = true;

}

然后开始读取配置,然而读取配置的实现也是需要留给子类去实现的:

1
2
3
4
loadBeanDefinitions(beanFactory);

protected abstract void loadBeanDefinitions(DefaultListableBeanFactory beanFactory)
throws BeansException, IOException;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {
// Create a new XmlBeanDefinitionReader for the given BeanFactory.
XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);

// Configure the bean definition reader with this context's
// resource loading environment.
beanDefinitionReader.setEnvironment(this.getEnvironment());
beanDefinitionReader.setResourceLoader(this);
beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));

// Allow a subclass to provide custom initialization of the reader,
// then proceed with actually loading the bean definitions.
initBeanDefinitionReader(beanDefinitionReader);
loadBeanDefinitions(beanDefinitionReader);
}

OK,子类要准备开始读取配置了。不过读取的实现和上一篇大同小异,我就不再重复了。 还记得我们刚刚在哪里吗,我们在 AbstractApplicationContext#refresh 进入了 ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); 这句话,接下来要进一步加工了。

2.3.3 AbstractApplicationContext#prepareBeanFactory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
protected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) {
// Tell the internal bean factory to use the context's class loader etc.
beanFactory.setBeanClassLoader(getClassLoader());
beanFactory.setBeanExpressionResolver(new StandardBeanExpressionResolver(beanFactory.getBeanClassLoader()));
beanFactory.addPropertyEditorRegistrar(new ResourceEditorRegistrar(this, getEnvironment()));

// 注册事件回调
beanFactory.addBeanPostProcessor(new ApplicationContextAwareProcessor(this));
// 实现这些接口的类不允许被注入
// 他们的作用只是用来回调
beanFactory.ignoreDependencyInterface(EnvironmentAware.class);
beanFactory.ignoreDependencyInterface(EmbeddedValueResolverAware.class);
beanFactory.ignoreDependencyInterface(ResourceLoaderAware.class);
beanFactory.ignoreDependencyInterface(ApplicationEventPublisherAware.class);
beanFactory.ignoreDependencyInterface(MessageSourceAware.class);
beanFactory.ignoreDependencyInterface(ApplicationContextAware.class);

// 手动注册一些必要的接口的实现类,比如监听器,可以用于回调
beanFactory.registerResolvableDependency(BeanFactory.class, beanFactory);
beanFactory.registerResolvableDependency(ResourceLoader.class, this);
beanFactory.registerResolvableDependency(ApplicationEventPublisher.class, this);
beanFactory.registerResolvableDependency(ApplicationContext.class, this);

// 内部类调用的事件回调
beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(this));

// 指定类在加载时织入AOP操作
if (beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) {
beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));
// Set a temporary ClassLoader for type matching.
beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
}

// 一些记录环境信息的Bean
if (!beanFactory.containsLocalBean(ENVIRONMENT_BEAN_NAME)) {
beanFactory.registerSingleton(ENVIRONMENT_BEAN_NAME, getEnvironment());
}
if (!beanFactory.containsLocalBean(SYSTEM_PROPERTIES_BEAN_NAME)) {
beanFactory.registerSingleton(SYSTEM_PROPERTIES_BEAN_NAME, getEnvironment().getSystemProperties());
}
if (!beanFactory.containsLocalBean(SYSTEM_ENVIRONMENT_BEAN_NAME)) {
beanFactory.registerSingleton(SYSTEM_ENVIRONMENT_BEAN_NAME, getEnvironment().getSystemEnvironment());
}
}

2.3.4 AbstractApplicationContext#postProcessBeanFactory

这个函数是留给子类实现,这时候 BeanFactory 中的 BeanDefinition 已经全部注册完毕,子类想删除新增或者其他操作都可以继续在这里实现。

2.3.5 AbstractApplicationContext#invokeBeanFactoryPostProcessors

来到这一步,说明 BeanFactory 一切准备就绪,这时候开始调用我们项目中注册到 BeanFactory 中的 BeanFactoryPostProcessors 处理器。 这一步呢,在我们项目中的应用就是,开始自动向权限中心注册当前项目中的用于管理数据的接口,以便项目更新即可分配新的权限。

2.3.6 AbstractApplicationContext#registerBeanPostProcessors

这一步是注册用户配置的实现 BeanPostProcessor 的实现类,用于监听每一个 Bean 实例化的前后所需要做的操作。

2.3.7 其他初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 初始化i18n资源的转换接口
initMessageSource();

// 初始化当前Context的消息广播
initApplicationEventMulticaster();

// 子类可覆写的其他refresh事件
onRefresh();

// 检查、注册监听器
registerListeners();

// 初始化非 lazy-init 的 Bean,如果配置了 lazy-init=false 则
// 配置的 Bean 以及关联的 Bean 会在这个时候被初始化
finishBeanFactoryInitialization(beanFactory);

// 广播事件、清理初始化中的缓存
finishRefresh();

finally {
// 清理初始化过程中用到的 Class 缓存
resetCommonCaches();
}

2.4 初始化配置类配置的Bean

在上面我没有看到配置类配置的 Bean,因为我在 debug 的时候,前几步观察 BeanFactory 的情况,都没看到我注册的 MyBean,后面就跳过了。 然而我错了,我发现在 getBean 的时候,我的 MyBean 他居然已经准备好了,所以肯定是在上面某个环节偷偷给我注册进去了的。 所以我现在需要返回去看看是哪一步进入的。 通过观察,我发现是在 invokeBeanFactoryPostProcessors(beanFactory); 这句话之后发生了 BeanDefinitionMap 的长度变化。 所以现在要看这句话做了什么事情。

其实因为在我想象中,xml 属于一种解析方式,javaConfig 应该跟 xml 平行的属于另外一种,但是貌似完全不是。Spring 是通过 BeanFactoryPostProcessor 来处理的。也就是说 SpringBoot 他启动貌似不需要解析器?

OK,来看下这个方法做了什么事情:

1
2
3
4
5
6
7
8
9
10
protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());

// Detect a LoadTimeWeaver and prepare for weaving, if found in the meantime
// (e.g. through an @Bean method registered by ConfigurationClassPostProcessor)
if (beanFactory.getTempClassLoader() == null && beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) {
beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));
beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
}
}

下面那句话说了,是织入运行时处理器,那么肯定是在第一句话就做了注册的。继续进入观察。 getBeanFactoryPostProcessors() 在目前的状态,是一个空的集合,所以我们现在需要进入看看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
public static void invokeBeanFactoryPostProcessors(
ConfigurableListableBeanFactory beanFactory, List<BeanFactoryPostProcessor> beanFactoryPostProcessors) {

// Invoke BeanDefinitionRegistryPostProcessors first, if any.
Set<String> processedBeans = new HashSet<>();

if (beanFactory instanceof BeanDefinitionRegistry) {
BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
List<BeanFactoryPostProcessor> regularPostProcessors = new ArrayList<>();
List<BeanDefinitionRegistryPostProcessor> registryProcessors = new ArrayList<>();

// 因为我们目前没有任何的 beanFactoryPostProcessors 所以这个循环并不会进入。
for (BeanFactoryPostProcessor postProcessor : beanFactoryPostProcessors) {
if (postProcessor instanceof BeanDefinitionRegistryPostProcessor) {
BeanDefinitionRegistryPostProcessor registryProcessor =
(BeanDefinitionRegistryPostProcessor) postProcessor;
registryProcessor.postProcessBeanDefinitionRegistry(registry);
registryProcessors.add(registryProcessor);
}
else {
regularPostProcessors.add(postProcessor);
}
}

// Do not initialize FactoryBeans here: We need to leave all regular beans
// uninitialized to let the bean factory post-processors apply to them!
// Separate between BeanDefinitionRegistryPostProcessors that implement
// PriorityOrdered, Ordered, and the rest.
// 在这里不初始化 FactoryBeans 从而可以让 beanfactory 的 post-processors 来处理他们
// 因为下面需要排序这些配置 Bean 的执行顺序。
List<BeanDefinitionRegistryPostProcessor> currentRegistryProcessors = new ArrayList<>();

// 开始排序这些Bean的顺序
String[] postProcessorNames =
beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);
for (String ppName : postProcessorNames) {
if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
processedBeans.add(ppName);
}
}
sortPostProcessors(currentRegistryProcessors, beanFactory);
registryProcessors.addAll(currentRegistryProcessors);
// 开始执行这些处理器!
invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry);
currentRegistryProcessors.clear();

// Next, invoke the BeanDefinitionRegistryPostProcessors that implement Ordered.
postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);
for (String ppName : postProcessorNames) {
if (!processedBeans.contains(ppName) && beanFactory.isTypeMatch(ppName, Ordered.class)) {
currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
processedBeans.add(ppName);
}
}
sortPostProcessors(currentRegistryProcessors, beanFactory);
registryProcessors.addAll(currentRegistryProcessors);
// ---> 在这里开始执行配置的装配
invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry);

/*
// 循环所有的 postProcessors 开始调用。目前只有一个 BeanDefinitionRegistryPostProcessor
// 是在 applicationContext 准备工作中加入的
private static void invokeBeanDefinitionRegistryPostProcessors(
Collection<? extends BeanDefinitionRegistryPostProcessor> postProcessors, BeanDefinitionRegistry registry) {

for (BeanDefinitionRegistryPostProcessor postProcessor : postProcessors) {
postProcessor.postProcessBeanDefinitionRegistry(registry);
}
}
*/

currentRegistryProcessors.clear();

/*
下面这些目前可以先不用看了。是用来执行其他 BeanDefinitionRegistryPostProcessors
因为我目前的主要点在于怎么解析我的配置类。
*/
// Finally, invoke all other BeanDefinitionRegistryPostProcessors until no further ones appear.
boolean reiterate = true;
while (reiterate) {
reiterate = false;
postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);
for (String ppName : postProcessorNames) {
if (!processedBeans.contains(ppName)) {
currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
processedBeans.add(ppName);
reiterate = true;
}
}
sortPostProcessors(currentRegistryProcessors, beanFactory);
registryProcessors.addAll(currentRegistryProcessors);
invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry);
currentRegistryProcessors.clear();
}

// Now, invoke the postProcessBeanFactory callback of all processors handled so far.
invokeBeanFactoryPostProcessors(registryProcessors, beanFactory);
invokeBeanFactoryPostProcessors(regularPostProcessors, beanFactory);
}

else {
// Invoke factory processors registered with the context instance.
invokeBeanFactoryPostProcessors(beanFactoryPostProcessors, beanFactory);
}

// Do not initialize FactoryBeans here: We need to leave all regular beans
// uninitialized to let the bean factory post-processors apply to them!
String[] postProcessorNames =
beanFactory.getBeanNamesForType(BeanFactoryPostProcessor.class, true, false);

// Separate between BeanFactoryPostProcessors that implement PriorityOrdered,
// Ordered, and the rest.
List<BeanFactoryPostProcessor> priorityOrderedPostProcessors = new ArrayList<>();
List<String> orderedPostProcessorNames = new ArrayList<>();
List<String> nonOrderedPostProcessorNames = new ArrayList<>();
for (String ppName : postProcessorNames) {
if (processedBeans.contains(ppName)) {
// skip - already processed in first phase above
}
else if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
priorityOrderedPostProcessors.add(beanFactory.getBean(ppName, BeanFactoryPostProcessor.class));
}
else if (beanFactory.isTypeMatch(ppName, Ordered.class)) {
orderedPostProcessorNames.add(ppName);
}
else {
nonOrderedPostProcessorNames.add(ppName);
}
}

// First, invoke the BeanFactoryPostProcessors that implement PriorityOrdered.
sortPostProcessors(priorityOrderedPostProcessors, beanFactory);
invokeBeanFactoryPostProcessors(priorityOrderedPostProcessors, beanFactory);

// Next, invoke the BeanFactoryPostProcessors that implement Ordered.
List<BeanFactoryPostProcessor> orderedPostProcessors = new ArrayList<>(orderedPostProcessorNames.size());
for (String postProcessorName : orderedPostProcessorNames) {
orderedPostProcessors.add(beanFactory.getBean(postProcessorName, BeanFactoryPostProcessor.class));
}
sortPostProcessors(orderedPostProcessors, beanFactory);
invokeBeanFactoryPostProcessors(orderedPostProcessors, beanFactory);

// Finally, invoke all other BeanFactoryPostProcessors.
List<BeanFactoryPostProcessor> nonOrderedPostProcessors = new ArrayList<>(nonOrderedPostProcessorNames.size());
for (String postProcessorName : nonOrderedPostProcessorNames) {
nonOrderedPostProcessors.add(beanFactory.getBean(postProcessorName, BeanFactoryPostProcessor.class));
}
invokeBeanFactoryPostProcessors(nonOrderedPostProcessors, beanFactory);

// Clear cached merged bean definitions since the post-processors might have
// modified the original metadata, e.g. replacing placeholders in values...
beanFactory.clearMetadataCache();
}

好我现在要在装配那部分开始进入了,因为 Spring 在开始的时候调用了 ConfigurationClassPostProcessorpostProcessBeanDefinitionRegistry 方法,现在要看看这个方法做了什么事情。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
int registryId = System.identityHashCode(registry);
if (this.registriesPostProcessed.contains(registryId)) {
throw new IllegalStateException(
"postProcessBeanDefinitionRegistry already called on this post-processor against " + registry);
}
if (this.factoriesPostProcessed.contains(registryId)) {
throw new IllegalStateException(
"postProcessBeanFactory already called on this post-processor against " + registry);
}
this.registriesPostProcessed.add(registryId);

// 开始解析 ConfigBean 的内容
processConfigBeanDefinitions(registry);
}

public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
List<BeanDefinitionHolder> configCandidates = new ArrayList<>();
String[] candidateNames = registry.getBeanDefinitionNames();

// 开始拿 registry 中的所有 Bean 一顿循环。
for (String beanName : candidateNames) {
BeanDefinition beanDef = registry.getBeanDefinition(beanName);
if (beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE) != null) {
if (logger.isDebugEnabled()) {
logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);
}
}
// 如果 Bean 定义上有 @Configuration 加入 configCandidates 集合中
else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
}
}

// Return immediately if no @Configuration classes were found
if (configCandidates.isEmpty()) {
return;
}

// 排序,但是目前这一步并不需要
configCandidates.sort((bd1, bd2) -> {
int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition());
int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition());
return Integer.compare(i1, i2);
});

// Detect any custom bean name generation strategy supplied through the enclosing application context
SingletonBeanRegistry sbr = null;
if (registry instanceof SingletonBeanRegistry) {
sbr = (SingletonBeanRegistry) registry;
if (!this.localBeanNameGeneratorSet) {
BeanNameGenerator generator = (BeanNameGenerator) sbr.getSingleton(
AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR);
if (generator != null) {
this.componentScanBeanNameGenerator = generator;
this.importBeanNameGenerator = generator;
}
}
}

if (this.environment == null) {
this.environment = new StandardEnvironment();
}

// 开始使用 ConfigurationClassParser 来解析配置类
ConfigurationClassParser parser = new ConfigurationClassParser(
this.metadataReaderFactory, this.problemReporter, this.environment,
this.resourceLoader, this.componentScanBeanNameGenerator, registry);

Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());
do {
parser.parse(candidates);
parser.validate();

Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
configClasses.removeAll(alreadyParsed);

// 使用 ConfigurationClassBeanDefinitionReader 来解析配置类
// 这一步就跟 xml reader 一样,解析里面各个标签
if (this.reader == null) {
this.reader = new ConfigurationClassBeanDefinitionReader(
registry, this.sourceExtractor, this.resourceLoader, this.environment,
this.importBeanNameGenerator, parser.getImportRegistry());
}
this.reader.loadBeanDefinitions(configClasses);
alreadyParsed.addAll(configClasses);

candidates.clear();
if (registry.getBeanDefinitionCount() > candidateNames.length) {
String[] newCandidateNames = registry.getBeanDefinitionNames();
Set<String> oldCandidateNames = new HashSet<>(Arrays.asList(candidateNames));
Set<String> alreadyParsedClasses = new HashSet<>();
for (ConfigurationClass configurationClass : alreadyParsed) {
alreadyParsedClasses.add(configurationClass.getMetadata().getClassName());
}
for (String candidateName : newCandidateNames) {
if (!oldCandidateNames.contains(candidateName)) {
BeanDefinition bd = registry.getBeanDefinition(candidateName);
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, this.metadataReaderFactory) &&
!alreadyParsedClasses.contains(bd.getBeanClassName())) {
candidates.add(new BeanDefinitionHolder(bd, candidateName));
}
}
}
candidateNames = newCandidateNames;
}
}
while (!candidates.isEmpty());

// Register the ImportRegistry as a bean in order to support ImportAware @Configuration classes
if (sbr != null && !sbr.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) {
sbr.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry());
}

if (this.metadataReaderFactory instanceof CachingMetadataReaderFactory) {
// Clear cache in externally provided MetadataReaderFactory; this is a no-op
// for a shared cache since it'll be cleared by the ApplicationContext.
((CachingMetadataReaderFactory) this.metadataReaderFactory).clearCache();
}
}

首先看看 ConfigurationClassParser 是怎么解析配置的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
ConfigurationClassParser#parse
public void parse(Set<BeanDefinitionHolder> configCandidates) {
for (BeanDefinitionHolder holder : configCandidates) {
BeanDefinition bd = holder.getBeanDefinition();
try {
if (bd instanceof AnnotatedBeanDefinition) {
parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
}
else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) {
parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName());
}
else {
// 直接进入到这里解析(其实上面看起来就是强转之前的判断)
parse(bd.getBeanClassName(), holder.getBeanName());
}
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex);
}
}

this.deferredImportSelectorHandler.process();
}

parse 里边先根据环境,简单封装了配置类的信息以及其他的相关信息 MetadataReader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
protected final void parse(@Nullable String className, String beanName) throws IOException {
Assert.notNull(className, "No bean class name for configuration class bean definition");
MetadataReader reader = this.metadataReaderFactory.getMetadataReader(className);
processConfigurationClass(new ConfigurationClass(reader, beanName));
}
protected void processConfigurationClass(ConfigurationClass configClass) throws IOException {
if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {
return;
}

ConfigurationClass existingClass = this.configurationClasses.get(configClass);
if (existingClass != null) {
if (configClass.isImported()) {
if (existingClass.isImported()) {
existingClass.mergeImportedBy(configClass);
}
// Otherwise ignore new imported config class; existing non-imported class overrides it.
return;
}
else {
// Explicit bean definition found, probably replacing an import.
// Let's remove the old one and go with the new one.
this.configurationClasses.remove(configClass);
this.knownSuperclasses.values().removeIf(configClass::equals);
}
}

// 循环解析到父级 但是我们现在
SourceClass sourceClass = asSourceClass(configClass);
do {
// 这里面解析了 @ComponentScan @Import 等注解,见下面,如果有父级,会返回父级,否则返回NULL
sourceClass = doProcessConfigurationClass(configClass, sourceClass);
}
while (sourceClass != null);

this.configurationClasses.put(configClass, configClass);
}
protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)
throws IOException {

if (configClass.getMetadata().isAnnotated(Component.class.getName())) {
// 解析内部类
processMemberClasses(configClass, sourceClass);
}

// 解析配置类(SpringBoot 熟悉的写配置自动提示那种配置)
for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), PropertySources.class,
org.springframework.context.annotation.PropertySource.class)) {
if (this.environment instanceof ConfigurableEnvironment) {
processPropertySource(propertySource);
}
else {
logger.info("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +
"]. Reason: Environment must implement ConfigurableEnvironment");
}
}

// 解析 @ComponentScan 这块打算后面再看了
Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
if (!componentScans.isEmpty() &&
!this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
for (AnnotationAttributes componentScan : componentScans) {
// The config class is annotated with @ComponentScan -> perform the scan immediately
Set<BeanDefinitionHolder> scannedBeanDefinitions =
this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
// Check the set of scanned definitions for any further config classes and parse recursively if needed
for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
if (bdCand == null) {
bdCand = holder.getBeanDefinition();
}
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
parse(bdCand.getBeanClassName(), holder.getBeanName());
}
}
}
}

// 解析 @Import 注解
processImports(configClass, sourceClass, getImports(sourceClass), true);

// 解析 @ImportResource 注解
AnnotationAttributes importResource =
AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class);
if (importResource != null) {
String[] resources = importResource.getStringArray("locations");
Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader");
for (String resource : resources) {
String resolvedResource = this.environment.resolveRequiredPlaceholders(resource);
configClass.addImportedResource(resolvedResource, readerClass);
}
}

// 解析 @Bean ,不过现在只是整理配置了 @Bean 的方法
Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);
for (MethodMetadata methodMetadata : beanMethods) {
configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
}

// Process default methods on interfaces
processInterfaces(configClass, sourceClass);

// Process superclass, if any
if (sourceClass.getMetadata().hasSuperClass()) {
String superclass = sourceClass.getMetadata().getSuperClassName();
if (superclass != null && !superclass.startsWith("java") &&
!this.knownSuperclasses.containsKey(superclass)) {
this.knownSuperclasses.put(superclass, configClass);
// Superclass found, return its annotation metadata and recurse
return sourceClass.getSuperClass();
}
}

// No superclass -> processing is complete
return null;
}

可不可以这么说,在使用 JavaBean 进行配置的时候,Spring 做了一个能够解读里面每一句的意思的类编译器。 接下来,回到 ConfigurationClassPostProcessorprocessConfigBeanDefinitions 里边。调用了 parser.validate(); 主要验证 cglib 生成的配置类。 紧接着开始解析 this.reader.loadBeanDefinitions(configClasses);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
public void loadBeanDefinitions(Set<ConfigurationClass> configurationModel) {
// 我们知道,Configurator是可以增加条件判断的
TrackedConditionEvaluator trackedConditionEvaluator = new TrackedConditionEvaluator();
for (ConfigurationClass configClass : configurationModel) {
// 开始读取 BeanDefinitions
loadBeanDefinitionsForConfigurationClass(configClass, trackedConditionEvaluator);
}
}
private void loadBeanDefinitionsForConfigurationClass(
ConfigurationClass configClass, TrackedConditionEvaluator trackedConditionEvaluator) {

// 如果条线显示,不需要启动此配置,则将其从 BeanFactory 中移除掉
if (trackedConditionEvaluator.shouldSkip(configClass)) {
String beanName = configClass.getBeanName();
if (StringUtils.hasLength(beanName) && this.registry.containsBeanDefinition(beanName)) {
this.registry.removeBeanDefinition(beanName);
}
this.importRegistry.removeImportingClass(configClass.getMetadata().getClassName());
return;
}

if (configClass.isImported()) {
registerBeanDefinitionForImportedConfigurationClass(configClass);
}
// 开始解析每个被 @Bean 修饰的方法
for (BeanMethod beanMethod : configClass.getBeanMethods()) {
loadBeanDefinitionsForBeanMethod(beanMethod);
}

// 读取完成后,读取其他资源
loadBeanDefinitionsFromImportedResources(configClass.getImportedResources());
loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars());
}
private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) {
ConfigurationClass configClass = beanMethod.getConfigurationClass();
MethodMetadata metadata = beanMethod.getMetadata();
String methodName = metadata.getMethodName();

// 继续条件判断,判断是否需要创建 Bean
if (this.conditionEvaluator.shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN)) {
configClass.skippedBeanMethods.add(methodName);
return;
}
if (configClass.skippedBeanMethods.contains(methodName)) {
return;
}

AnnotationAttributes bean = AnnotationConfigUtils.attributesFor(metadata, Bean.class);
Assert.state(bean != null, "No @Bean annotation attributes");

// Consider name and any aliases
List<String> names = new ArrayList<>(Arrays.asList(bean.getStringArray("name")));
// 这一步让我知道了,方法名就是 bean 的名字= =
String beanName = (!names.isEmpty() ? names.remove(0) : methodName);

// Register aliases even when overridden
for (String alias : names) {
this.registry.registerAlias(beanName, alias);
}

// 判断 xml 配置的 override 是否跟 JavaConfig 冲突了
if (isOverriddenByExistingDefinition(beanMethod, beanName)) {
if (beanName.equals(beanMethod.getConfigurationClass().getBeanName())) {
throw new BeanDefinitionStoreException(beanMethod.getConfigurationClass().getResource().getDescription(),
beanName, "Bean name derived from @Bean method '" + beanMethod.getMetadata().getMethodName() +
"' clashes with bean name for containing configuration class; please make those names unique!");
}
return;
}

ConfigurationClassBeanDefinition beanDef = new ConfigurationClassBeanDefinition(configClass, metadata);
beanDef.setResource(configClass.getResource());
beanDef.setSource(this.sourceExtractor.extractSource(metadata, configClass.getResource()));

// 非 static 方法
if (metadata.isStatic()) {
// static @Bean method
if (configClass.getMetadata() instanceof StandardAnnotationMetadata) {
beanDef.setBeanClass(((StandardAnnotationMetadata) configClass.getMetadata()).getIntrospectedClass());
}
else {
beanDef.setBeanClassName(configClass.getMetadata().getClassName());
}
beanDef.setUniqueFactoryMethodName(methodName);
}
else {
// 我的配置被当成 FactoryBean 来处理了
beanDef.setFactoryBeanName(configClass.getBeanName());
beanDef.setUniqueFactoryMethodName(methodName);
}

if (metadata instanceof StandardMethodMetadata) {
beanDef.setResolvedFactoryMethod(((StandardMethodMetadata) metadata).getIntrospectedMethod());
}

beanDef.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR);
beanDef.setAttribute(org.springframework.beans.factory.annotation.RequiredAnnotationBeanPostProcessor.
SKIP_REQUIRED_CHECK_ATTRIBUTE, Boolean.TRUE);

AnnotationConfigUtils.processCommonDefinitionAnnotations(beanDef, metadata);

// 开始解析一系列的属性
Autowire autowire = bean.getEnum("autowire");
if (autowire.isAutowire()) {
beanDef.setAutowireMode(autowire.value());
}

boolean autowireCandidate = bean.getBoolean("autowireCandidate");
if (!autowireCandidate) {
beanDef.setAutowireCandidate(false);
}

String initMethodName = bean.getString("initMethod");
if (StringUtils.hasText(initMethodName)) {
beanDef.setInitMethodName(initMethodName);
}

String destroyMethodName = bean.getString("destroyMethod");
beanDef.setDestroyMethodName(destroyMethodName);

// Consider scoping
ScopedProxyMode proxyMode = ScopedProxyMode.NO;
AnnotationAttributes attributes = AnnotationConfigUtils.attributesFor(metadata, Scope.class);
if (attributes != null) {
beanDef.setScope(attributes.getString("value"));
proxyMode = attributes.getEnum("proxyMode");
if (proxyMode == ScopedProxyMode.DEFAULT) {
proxyMode = ScopedProxyMode.NO;
}
}

// Replace the original bean definition with the target one, if necessary
BeanDefinition beanDefToRegister = beanDef;
if (proxyMode != ScopedProxyMode.NO) {
BeanDefinitionHolder proxyDef = ScopedProxyCreator.createScopedProxy(
new BeanDefinitionHolder(beanDef, beanName), this.registry,
proxyMode == ScopedProxyMode.TARGET_CLASS);
beanDefToRegister = new ConfigurationClassBeanDefinition(
(RootBeanDefinition) proxyDef.getBeanDefinition(), configClass, metadata);
}

if (logger.isTraceEnabled()) {
logger.trace(String.format("Registering bean definition for @Bean method %s.%s()",
configClass.getMetadata().getClassName(), beanName));
}

// 好了,第一篇解析 xml 的时候已经说过了,复用了之前的方法
this.registry.registerBeanDefinition(beanName, beanDefToRegister);
}

好了,现在回到上面刚刚的地方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
/* 
下面这些目前可以先不用看了。是用来执行其他 BeanDefinitionRegistryPostProcessors
因为我目前的主要点在于怎么解析我的配置类。
*/
// 开始运行其他的 BeanDefinitionRegistryPostProcessor
boolean reiterate = true;
while (reiterate) {
reiterate = false;
postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);
for (String ppName : postProcessorNames) {
if (!processedBeans.contains(ppName)) {
currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
processedBeans.add(ppName);
reiterate = true;
}
}
sortPostProcessors(currentRegistryProcessors, beanFactory);
registryProcessors.addAll(currentRegistryProcessors);
invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry);
currentRegistryProcessors.clear();
}

// Now, invoke the postProcessBeanFactory callback of all processors handled so far.
invokeBeanFactoryPostProcessors(registryProcessors, beanFactory);
invokeBeanFactoryPostProcessors(regularPostProcessors, beanFactory);
}

else {
// Invoke factory processors registered with the context instance.
invokeBeanFactoryPostProcessors(beanFactoryPostProcessors, beanFactory);
}

// 然后按照,配置了 PriorityOrdered、配置了 Ordered、未配置顺序的执行顺序来执行这些生命周期回调函数
// 其中使用了 processedBeans 来记录已经执行过的 processor,如果执行过的则会跳过
String[] postProcessorNames =
beanFactory.getBeanNamesForType(BeanFactoryPostProcessor.class, true, false);

// Separate between BeanFactoryPostProcessors that implement PriorityOrdered,
// Ordered, and the rest.
List<BeanFactoryPostProcessor> priorityOrderedPostProcessors = new ArrayList<>();
List<String> orderedPostProcessorNames = new ArrayList<>();
List<String> nonOrderedPostProcessorNames = new ArrayList<>();
for (String ppName : postProcessorNames) {
if (processedBeans.contains(ppName)) {
// skip - already processed in first phase above
}
else if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
priorityOrderedPostProcessors.add(beanFactory.getBean(ppName, BeanFactoryPostProcessor.class));
}
else if (beanFactory.isTypeMatch(ppName, Ordered.class)) {
orderedPostProcessorNames.add(ppName);
}
else {
nonOrderedPostProcessorNames.add(ppName);
}
}

// First, invoke the BeanFactoryPostProcessors that implement PriorityOrdered.
sortPostProcessors(priorityOrderedPostProcessors, beanFactory);
invokeBeanFactoryPostProcessors(priorityOrderedPostProcessors, beanFactory);

// Next, invoke the BeanFactoryPostProcessors that implement Ordered.
List<BeanFactoryPostProcessor> orderedPostProcessors = new ArrayList<>(orderedPostProcessorNames.size());
for (String postProcessorName : orderedPostProcessorNames) {
orderedPostProcessors.add(beanFactory.getBean(postProcessorName, BeanFactoryPostProcessor.class));
}
sortPostProcessors(orderedPostProcessors, beanFactory);
invokeBeanFactoryPostProcessors(orderedPostProcessors, beanFactory);

// Finally, invoke all other BeanFactoryPostProcessors.
List<BeanFactoryPostProcessor> nonOrderedPostProcessors = new ArrayList<>(nonOrderedPostProcessorNames.size());
for (String postProcessorName : nonOrderedPostProcessorNames) {
nonOrderedPostProcessors.add(beanFactory.getBean(postProcessorName, BeanFactoryPostProcessor.class));
}
invokeBeanFactoryPostProcessors(nonOrderedPostProcessors, beanFactory);

// Clear cached merged bean definitions since the post-processors might have
// modified the original metadata, e.g. replacing placeholders in values...
beanFactory.clearMetadataCache();

这串动作做好了以后,我们在 MyBeanConfiguration 里面配置的 MyBean.class,已经被注册到了当前的 BeanFactory 中去了。然后回到这里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
AbstractApplicationContext#refresh():
...
registerBeanPostProcessors(beanFactory);

// Initialize message source for this context.
initMessageSource();

// Initialize event multicaster for this context.
initApplicationEventMulticaster();

// Initialize other special beans in specific context subclasses.
onRefresh();

// Check for listener beans and register them.
registerListeners();

// 在这里实例化我们没有加上 @LazyInit 的对象,也就是 MyBean 在这里进行实例化
finishBeanFactoryInitialization(beanFactory);
...

2.5 初始化配置的Bean对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
// 注册数据转换器
if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME) &&
beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)) {
beanFactory.setConversionService(
beanFactory.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class));
}

// 注册配置文件解析器,解析注入配置文件的那种方式
if (!beanFactory.hasEmbeddedValueResolver()) {
beanFactory.addEmbeddedValueResolver(strVal -> getEnvironment().resolvePlaceholders(strVal));
}

// Initialize LoadTimeWeaverAware beans early to allow for registering their transformers early.
// AOP相关的先跳过
String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false);
for (String weaverAwareName : weaverAwareNames) {
getBean(weaverAwareName);
}

// Stop using the temporary ClassLoader for type matching.
beanFactory.setTempClassLoader(null);

// 缓存BeanDefinition的内容,这时候过后已经停止修改BeanDefinition的内容了
beanFactory.freezeConfiguration();

// 在这里做配置类中所有对象的初始化
beanFactory.preInstantiateSingletons();
}

DefaultListableBeanFactory#preInstantiateSingletons
@Override
public void preInstantiateSingletons() throws BeansException {
if (logger.isTraceEnabled()) {
logger.trace("Pre-instantiating singletons in " + this);
}

// Iterate over a copy to allow for init methods which in turn register new bean definitions.
// While this may not be part of the regular factory bootstrap, it does otherwise work fine.
List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);

// 便利所有非LazyInit的BeanNames,拿到BeanDefinition进行初始化。
for (String beanName : beanNames) {
RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
if (isFactoryBean(beanName)) {
Object bean = getBean(FACTORY_BEAN_PREFIX + beanName);
if (bean instanceof FactoryBean) {
final FactoryBean<?> factory = (FactoryBean<?>) bean;
boolean isEagerInit;
if (System.getSecurityManager() != null && factory instanceof SmartFactoryBean) {
isEagerInit = AccessController.doPrivileged((PrivilegedAction<Boolean>)
((SmartFactoryBean<?>) factory)::isEagerInit,
getAccessControlContext());
}
else {
isEagerInit = (factory instanceof SmartFactoryBean &&
((SmartFactoryBean<?>) factory).isEagerInit());
}
if (isEagerInit) {
getBean(beanName);
}
}
}
else {
// 在这里触发初始化,进去后直接到AbstractBeanFactory#getBean
// 已经很熟悉的函数了,不再细说。
getBean(beanName);
}
}
}

// 触发post-initialization回调函数
for (String beanName : beanNames) {
Object singletonInstance = getSingleton(beanName);
if (singletonInstance instanceof SmartInitializingSingleton) {
final SmartInitializingSingleton smartSingleton = (SmartInitializingSingleton) singletonInstance;
if (System.getSecurityManager() != null) {
AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
smartSingleton.afterSingletonsInstantiated();
return null;
}, getAccessControlContext());
}
else {
smartSingleton.afterSingletonsInstantiated();
}
}
}
}

结 好了,我们已经可以看到,application 是一个 BeanFactory 但是他又没有去继承我们之前看到的 BeanFactory 的实现类。为啥,他并不是一个单纯的 BeanFactory 实现,而是聚合了 BeanFactory 实现类,来实现 BeanFactory 的所有功能,所以,它拥有 BeanFactory 接口的所有功能。(还记得那句话吗?接口表示实现类会什么(class has xxx),继承表示实现类是什么(class is xxx))

2.1 ClassPathXmlApplicationContext构造器

好,首先从这段代码开始:

1
BeanFactory bf = new ClassPathXmlApplicationContext("applicationContext.xml");

ClassPathXmlApplicationContext 其实是一个简单到不能再简单的类了,他的主要功能就是记录用户传递的 applicationContext.xml 的路径,是一个字符串数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ClassPathXmlApplicationContext extends AbstractXmlApplicationContext {

@Nullable
private Resource[] configResources;

/** 这是我们进入的时候调用的构造方法 */
public ClassPathXmlApplicationContext(String... configLocations) throws BeansException {
// 调用了下面的构造方法
this(configLocations, true, null);
}

public ClassPathXmlApplicationContext(
String[] configLocations, boolean refresh, @Nullable ApplicationContext parent)
throws BeansException {
// 这里是一个 Null,我就不进去看了,反正就是之前熟悉的
// 设置 parentBeanFactory
super(parent);
setConfigLocations(configLocations);
if (refresh) {
refresh();
}
}

}

OK,接下来我们进入这句话:setConfigLocations(configLocations);

2.2 AbstractRefreshableConfigApplicationContext#setConfigLocations

这句话的实现是在父级 AbstractRefreshableConfigApplicationContext 实现的,先简单了解下这句话是做什么用的,这句话是设置我 AbstractRefreshableConfigApplicationContext 的配置路径的属性,顺便通过环境的配置,把类似于 application-${someProp} 的配置,给还原成我们配置再环境的内容。 先看下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public abstract class AbstractRefreshableConfigApplicationContext extends AbstractRefreshableApplicationContext
implements BeanNameAware, InitializingBean {

public void setConfigLocations(@Nullable String... locations) {
if (locations != null) {
Assert.noNullElements(locations, "Config locations must not be null");
this.configLocations = new String[locations.length];
for (int i = 0; i < locations.length; i++) {
// 重点要跟着这一句走
this.configLocations[i] = resolvePath(locations[i]).trim();
}
}
else {
this.configLocations = null;
}
}

protected String resolvePath(String path) {
return getEnvironment().resolveRequiredPlaceholders(path);
}

/*
getEnvironment()这一句还是在父类 AbstractApplicationContext 实现的:
protected ConfigurableEnvironment createEnvironment() {
return new StandardEnvironment();
}
*/

}

简单的创建了一个 StandardEnvironment 对象,我想这是因为我在类似于 main 方法运行的缘故,如果是 web 环境有另外一种实现 StandardServletEnvironmentStandardServletEnvironment#resolveRequiredPlaceholders 代码则是把这件事情交给 core 里面的工具类 PropertyPlaceholderHelper#parseStringValue 来实现,由于这件事情不是我要看的重点,所以我先直接跳过去好了。 然后直接下一步,if (refresh) {...} 因为构造器直接写死传递了一个 true,所以现在需要直接进入 refresh() 方法来做。

2.3 AbstractApplicationContext#refresh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
prepareRefresh();

// Tell the subclass to refresh the internal bean factory.
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);

try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);

// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);

// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);

// Initialize message source for this context.
initMessageSource();

// Initialize event multicaster for this context.
initApplicationEventMulticaster();

// Initialize other special beans in specific context subclasses.
onRefresh();

// Check for listener beans and register them.
registerListeners();

// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);

// Last step: publish corresponding event.
finishRefresh();
}

catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}

// Destroy already created singletons to avoid dangling resources.
destroyBeans();

// Reset 'active' flag.
cancelRefresh(ex);

// Propagate exception to caller.
throw ex;
}

finally {
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
resetCommonCaches();
}
}
}

AbstractApplicationContext 看起来就是一个快要成型的 ApplicationContext 了,这里 refresh() 方法看起来就是调用一些其他方法来做启动的。

2.3.1 AbstractApplicationContext#prepareRefresh

那首先就是准备环境:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
protected void prepareRefresh() {
// Switch to active.
this.startupDate = System.currentTimeMillis();
// 设置非关闭状态
this.closed.set(false);
// 已激活状态
this.active.set(true);

if (logger.isDebugEnabled()) {
if (logger.isTraceEnabled()) {
logger.trace("Refreshing " + this);
}
else {
logger.debug("Refreshing " + getDisplayName());
}
}

// 这是一个空的方法,允许子类重写,做一些其他属性源的准备。
initPropertySources();

// 验证用户设置的必要的属性,如果这些属性确实则会抛出异常
// 目前来说我没有设置任何必要的属性,所以这一步是空的实现
getEnvironment().validateRequiredProperties();

// 下面代码都是初始化 applicationListeners 的容器
// 也就是 applicationContext 启动前我们注册的事件监听器
if (this.earlyApplicationListeners == null) {
this.earlyApplicationListeners = new LinkedHashSet<>(this.applicationListeners);
}
else {
// Reset local application listeners to pre-refresh state.
this.applicationListeners.clear();
this.applicationListeners.addAll(this.earlyApplicationListeners);
}

// Allow for the collection of early ApplicationEvents,
// to be published once the multicaster is available...
this.earlyApplicationEvents = new LinkedHashSet<>();
}

初始化我感觉没做什么特别的事情,也就是初始化一些必要的容器。 接下来就是大事情了:

1
2
// Tell the subclass to refresh the internal bean factory.
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

创建一个 BeanFactory

2.3.2 AbstractApplicationContext#obtainFreshBeanFactory

1
2
3
4
5
6
7
8
protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
refreshBeanFactory();
return getBeanFactory();
}

protected abstract void refreshBeanFactory() throws BeansException, IllegalStateException;

public abstract ConfigurableListableBeanFactory getBeanFactory() throws IllegalStateException;

好了,这就是一个模板方法的设计模式,调用了两个需要子类实现的方法。

简述

回顾一下上一篇,使用的是一个简单的 BeanFactory 实现 XmlBeanFactory。这个容器已经是一个过时的容器了,因为他并不能实现除了注册 BeanDefinition 之外的事情(比如 i18n 访问资源(骚一点的话可以加载网络 Spring 配置文件) 应用事件)。所以,就有了 spring-context 的出现。 我们现在知道了,Spring 把载入、解析、注册解耦成不同的模块,所以我们现在可以大概的知道:

  1. 载入:我们可以使用不同的方式载入配置文件,然后配置解析方式,生成 BeanDefinition 注册到底层的 Bean 容器中;
  2. 解析:不同的配置文件解析方式有所不同,但是有个共同的目的就是解析成 BeanDefinition 注册到容器中去;
  3. 注册:这块 Spring 已经提供了 DefaultListableBeanFactory 这个成熟的实现,所以即使我们想通过其他配置文件来配置我们的 Spring 项目的话,不用慌,只要用它就好了(奥森!)

那么仔细想想,我们是不是可以实现由 JSON 格式配置文件来配置的 BeanFactory

一.一个可运行的DEMO

因为 Spring 现在支持 Java注解 来生成对象,所以我想在看 ApplicationContext 源码的时候,顺便把怎么从配置里面取出 Bean 的过程给看了,所以配置文件只配置了一个 Java配置类。

applicationContext.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
">

<bean class="cn.liweidan.confbean.MyBeanConfiguration"/>
<context:annotation-config/>
</beans>

MyBeanConfiguration.java

这个写法在 SpringBoot 横行霸道的时候应该不会陌生。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package cn.liweidan.confbean;

import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.context.annotation.Bean;

@Configurable
public class MyBeanConfiguration {

@Bean
public MyBean myBean() {
MyBean myBean = new MyBean();
myBean.setName("Weidan");
return myBean;
}

}

MyBean.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package cn.liweidan.confbean;

public class MyBean {

private String name;

public MyBean(String name) {
this.name = name;
}

public MyBean() {
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}

启动器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package cn.liweidan.confbean;

import org.junit.Assert;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class ConfigBeanTest {

@Test
public void testConfigBean() {
BeanFactory bf = new ClassPathXmlApplicationContext("applicationContext.xml");
MyBean bean = bf.getBean(MyBean.class);
Assert.assertNotNull(bean);
}

}

跑一下测试用例,恩!是绿色的感觉!(绿色?)

二.源码解析

其实我一直没有经验怎么写源码的解析,看得人才能容易看的明白,所以我就按照 debug 走的顺序来走了,在哪个类的哪个方法,都会放在三级标题上。

好了,我们已经可以看到,application 是一个 BeanFactory 但是他又没有去继承我们之前看到的 BeanFactory 的实现类。为啥,他并不是一个单纯的 BeanFactory 实现,而是聚合了 BeanFactory 实现类,来实现 BeanFactory 的所有功能,所以,它拥有 BeanFactory 接口的所有功能。(还记得那句话吗?接口表示实现类会什么(class has xxx),继承表示实现类是什么(class is xxx))

2.1 ClassPathXmlApplicationContext构造器

好,首先从这段代码开始:

1
BeanFactory bf = new ClassPathXmlApplicationContext("applicationContext.xml");

ClassPathXmlApplicationContext 其实是一个简单到不能再简单的类了,他的主要功能就是记录用户传递的 applicationContext.xml 的路径,是一个字符串数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ClassPathXmlApplicationContext extends AbstractXmlApplicationContext {

@Nullable
private Resource[] configResources;

/** 这是我们进入的时候调用的构造方法 */
public ClassPathXmlApplicationContext(String... configLocations) throws BeansException {
// 调用了下面的构造方法
this(configLocations, true, null);
}

public ClassPathXmlApplicationContext(
String[] configLocations, boolean refresh, @Nullable ApplicationContext parent)
throws BeansException {
// 这里是一个 Null,我就不进去看了,反正就是之前熟悉的
// 设置 parentBeanFactory
super(parent);
setConfigLocations(configLocations);
if (refresh) {
refresh();
}
}

}

OK,接下来我们进入这句话:setConfigLocations(configLocations);

2.2 AbstractRefreshableConfigApplicationContext#setConfigLocations

这句话的实现是在父级 AbstractRefreshableConfigApplicationContext 实现的,先简单了解下这句话是做什么用的,这句话是设置我 AbstractRefreshableConfigApplicationContext 的配置路径的属性,顺便通过环境的配置,把类似于 application-${someProp} 的配置,给还原成我们配置再环境的内容。 先看下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public abstract class AbstractRefreshableConfigApplicationContext extends AbstractRefreshableApplicationContext
implements BeanNameAware, InitializingBean {

public void setConfigLocations(@Nullable String... locations) {
if (locations != null) {
Assert.noNullElements(locations, "Config locations must not be null");
this.configLocations = new String[locations.length];
for (int i = 0; i < locations.length; i++) {
// 重点要跟着这一句走
this.configLocations[i] = resolvePath(locations[i]).trim();
}
}
else {
this.configLocations = null;
}
}

protected String resolvePath(String path) {
return getEnvironment().resolveRequiredPlaceholders(path);
}

/*
getEnvironment()这一句还是在父类 AbstractApplicationContext 实现的:
protected ConfigurableEnvironment createEnvironment() {
return new StandardEnvironment();
}
*/

}

简单的创建了一个 StandardEnvironment 对象,我想这是因为我在类似于 main 方法运行的缘故,如果是 web 环境有另外一种实现 StandardServletEnvironmentStandardServletEnvironment#resolveRequiredPlaceholders 代码则是把这件事情交给 core 里面的工具类 PropertyPlaceholderHelper#parseStringValue 来实现,由于这件事情不是我要看的重点,所以我先直接跳过去好了。 然后直接下一步,if (refresh) {...} 因为构造器直接写死传递了一个 true,所以现在需要直接进入 refresh() 方法来做。

2.3 AbstractApplicationContext#refresh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
prepareRefresh();

// Tell the subclass to refresh the internal bean factory.
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);

try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);

// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);

// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);

// Initialize message source for this context.
initMessageSource();

// Initialize event multicaster for this context.
initApplicationEventMulticaster();

// Initialize other special beans in specific context subclasses.
onRefresh();

// Check for listener beans and register them.
registerListeners();

// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);

// Last step: publish corresponding event.
finishRefresh();
}

catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}

// Destroy already created singletons to avoid dangling resources.
destroyBeans();

// Reset 'active' flag.
cancelRefresh(ex);

// Propagate exception to caller.
throw ex;
}

finally {
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
resetCommonCaches();
}
}
}

AbstractApplicationContext 看起来就是一个快要成型的 ApplicationContext 了,这里 refresh() 方法看起来就是调用一些其他方法来做启动的。

2.3.1 AbstractApplicationContext#prepareRefresh

那首先就是准备环境:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
protected void prepareRefresh() {
// Switch to active.
this.startupDate = System.currentTimeMillis();
// 设置非关闭状态
this.closed.set(false);
// 已激活状态
this.active.set(true);

if (logger.isDebugEnabled()) {
if (logger.isTraceEnabled()) {
logger.trace("Refreshing " + this);
}
else {
logger.debug("Refreshing " + getDisplayName());
}
}

// 这是一个空的方法,允许子类重写,做一些其他属性源的准备。
initPropertySources();

// 验证用户设置的必要的属性,如果这些属性确实则会抛出异常
// 目前来说我没有设置任何必要的属性,所以这一步是空的实现
getEnvironment().validateRequiredProperties();

// 下面代码都是初始化 applicationListeners 的容器
// 也就是 applicationContext 启动前我们注册的事件监听器
if (this.earlyApplicationListeners == null) {
this.earlyApplicationListeners = new LinkedHashSet<>(this.applicationListeners);
}
else {
// Reset local application listeners to pre-refresh state.
this.applicationListeners.clear();
this.applicationListeners.addAll(this.earlyApplicationListeners);
}

// Allow for the collection of early ApplicationEvents,
// to be published once the multicaster is available...
this.earlyApplicationEvents = new LinkedHashSet<>();
}

初始化我感觉没做什么特别的事情,也就是初始化一些必要的容器。 接下来就是大事情了:

1
2
// Tell the subclass to refresh the internal bean factory.
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

创建一个 BeanFactory

2.3.2 AbstractApplicationContext#obtainFreshBeanFactory

1
2
3
4
5
6
7
8
protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
refreshBeanFactory();
return getBeanFactory();
}

protected abstract void refreshBeanFactory() throws BeansException, IllegalStateException;

public abstract ConfigurableListableBeanFactory getBeanFactory() throws IllegalStateException;

好了,这就是一个模板方法的设计模式,调用了两个需要子类实现的方法。


接下来看看子类怎么创建的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Override
protected final void refreshBeanFactory() throws BeansException {
if (hasBeanFactory()) {
destroyBeans();
closeBeanFactory();
}
try {
DefaultListableBeanFactory beanFactory = createBeanFactory();
beanFactory.setSerializationId(getId());
customizeBeanFactory(beanFactory);
loadBeanDefinitions(beanFactory);
synchronized (this.beanFactoryMonitor) {
this.beanFactory = beanFactory;
}
}
catch (IOException ex) {
throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex);
}
}


@Override
public final ConfigurableListableBeanFactory getBeanFactory() {
synchronized (this.beanFactoryMonitor) {
if (this.beanFactory == null) {
throw new IllegalStateException("BeanFactory not initialized or already closed - " +
"call 'refresh' before accessing beans via the ApplicationContext");
}
return this.beanFactory;
}
}

OK,我们现在看到,首先检测当前是否有工厂实例存在,如果有,直接清理掉里面的数据。然后创建、配置、并且返回回去。在子类看来就只有做这几件事情。 那在当前类的 refreshBeanFactory 方法中,就是着重的看着,是怎么配置并且构建的就 ok 了。 首先看 DefaultListableBeanFactory beanFactory = createBeanFactory(); 这个:

1
2
3
protected DefaultListableBeanFactory createBeanFactory() {
return new DefaultListableBeanFactory(getInternalParentBeanFactory());
}

好吧,过! 然后开始设置 SerializationId 这个也不需要看。 下一个是 customizeBeanFactory(beanFactory);

1
2
3
4
5
6
7
8
protected void customizeBeanFactory(DefaultListableBeanFactory beanFactory) {
if (this.allowBeanDefinitionOverriding != null) {
beanFactory.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
}
if (this.allowCircularReferences != null) {
beanFactory.setAllowCircularReferences(this.allowCircularReferences);
}
}

这是两个自定义的属性,一个是是否允许覆盖 Bean 的定义,一个是是否允许相互依赖,但是这两个 this. 开头的都是 null,如果子类需要修改这两个值的话,则需要在子类调用相对应的 setter。因为没有调用,所以均使用 DefaultListableBeanFactory 默认设置的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory
implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable {

/** Whether to allow re-registration of a different definition with the same name. */
private boolean allowBeanDefinitionOverriding = true;

}

public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory
implements AutowireCapableBeanFactory {

/** Whether to automatically try to resolve circular references between beans. */
private boolean allowCircularReferences = true;

}

然后开始读取配置,然而读取配置的实现也是需要留给子类去实现的:

1
2
3
4
loadBeanDefinitions(beanFactory);

protected abstract void loadBeanDefinitions(DefaultListableBeanFactory beanFactory)
throws BeansException, IOException;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {
// Create a new XmlBeanDefinitionReader for the given BeanFactory.
XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);

// Configure the bean definition reader with this context's
// resource loading environment.
beanDefinitionReader.setEnvironment(this.getEnvironment());
beanDefinitionReader.setResourceLoader(this);
beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));

// Allow a subclass to provide custom initialization of the reader,
// then proceed with actually loading the bean definitions.
initBeanDefinitionReader(beanDefinitionReader);
loadBeanDefinitions(beanDefinitionReader);
}

OK,子类要准备开始读取配置了。不过读取的实现和上一篇大同小异,我就不再重复了。 还记得我们刚刚在哪里吗,我们在 AbstractApplicationContext#refresh 进入了 ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); 这句话,接下来要进一步加工了。

2.3.3 AbstractApplicationContext#prepareBeanFactory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
protected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) {
// Tell the internal bean factory to use the context's class loader etc.
beanFactory.setBeanClassLoader(getClassLoader());
beanFactory.setBeanExpressionResolver(new StandardBeanExpressionResolver(beanFactory.getBeanClassLoader()));
beanFactory.addPropertyEditorRegistrar(new ResourceEditorRegistrar(this, getEnvironment()));

// 注册事件回调
beanFactory.addBeanPostProcessor(new ApplicationContextAwareProcessor(this));
// 实现这些接口的类不允许被注入
// 他们的作用只是用来回调
beanFactory.ignoreDependencyInterface(EnvironmentAware.class);
beanFactory.ignoreDependencyInterface(EmbeddedValueResolverAware.class);
beanFactory.ignoreDependencyInterface(ResourceLoaderAware.class);
beanFactory.ignoreDependencyInterface(ApplicationEventPublisherAware.class);
beanFactory.ignoreDependencyInterface(MessageSourceAware.class);
beanFactory.ignoreDependencyInterface(ApplicationContextAware.class);

// 手动注册一些必要的接口的实现类,比如监听器,可以用于回调
beanFactory.registerResolvableDependency(BeanFactory.class, beanFactory);
beanFactory.registerResolvableDependency(ResourceLoader.class, this);
beanFactory.registerResolvableDependency(ApplicationEventPublisher.class, this);
beanFactory.registerResolvableDependency(ApplicationContext.class, this);

// 内部类调用的事件回调
beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(this));

// 指定类在加载时织入AOP操作
if (beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) {
beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));
// Set a temporary ClassLoader for type matching.
beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
}

// 一些记录环境信息的Bean
if (!beanFactory.containsLocalBean(ENVIRONMENT_BEAN_NAME)) {
beanFactory.registerSingleton(ENVIRONMENT_BEAN_NAME, getEnvironment());
}
if (!beanFactory.containsLocalBean(SYSTEM_PROPERTIES_BEAN_NAME)) {
beanFactory.registerSingleton(SYSTEM_PROPERTIES_BEAN_NAME, getEnvironment().getSystemProperties());
}
if (!beanFactory.containsLocalBean(SYSTEM_ENVIRONMENT_BEAN_NAME)) {
beanFactory.registerSingleton(SYSTEM_ENVIRONMENT_BEAN_NAME, getEnvironment().getSystemEnvironment());
}
}

2.3.4 AbstractApplicationContext#postProcessBeanFactory

这个函数是留给子类实现,这时候 BeanFactory 中的 BeanDefinition 已经全部注册完毕,子类想删除新增或者其他操作都可以继续在这里实现。

2.3.5 AbstractApplicationContext#invokeBeanFactoryPostProcessors

来到这一步,说明 BeanFactory 一切准备就绪,这时候开始调用我们项目中注册到 BeanFactory 中的 BeanFactoryPostProcessors 处理器。 这一步呢,在我们项目中的应用就是,开始自动向权限中心注册当前项目中的用于管理数据的接口,以便项目更新即可分配新的权限。

2.3.6 AbstractApplicationContext#registerBeanPostProcessors

这一步是注册用户配置的实现 BeanPostProcessor 的实现类,用于监听每一个 Bean 实例化的前后所需要做的操作。

2.3.7 其他初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 初始化i18n资源的转换接口
initMessageSource();

// 初始化当前Context的消息广播
initApplicationEventMulticaster();

// 子类可覆写的其他refresh事件
onRefresh();

// 检查、注册监听器
registerListeners();

// 初始化非 lazy-init 的 Bean,如果配置了 lazy-init=false 则
// 配置的 Bean 以及关联的 Bean 会在这个时候被初始化
finishBeanFactoryInitialization(beanFactory);

// 广播事件、清理初始化中的缓存
finishRefresh();

finally {
// 清理初始化过程中用到的 Class 缓存
resetCommonCaches();
}

2.4 初始化配置类配置的Bean

在上面我没有看到配置类配置的 Bean,因为我在 debug 的时候,前几步观察 BeanFactory 的情况,都没看到我注册的 MyBean,后面就跳过了。 然而我错了,我发现在 getBean 的时候,我的 MyBean 他居然已经准备好了,所以肯定是在上面某个环节偷偷给我注册进去了的。 所以我现在需要返回去看看是哪一步进入的。 通过观察,我发现是在 invokeBeanFactoryPostProcessors(beanFactory); 这句话之后发生了 BeanDefinitionMap 的长度变化。 所以现在要看这句话做了什么事情。

其实因为在我想象中,xml 属于一种解析方式,javaConfig 应该跟 xml 平行的属于另外一种,但是貌似完全不是。Spring 是通过 BeanFactoryPostProcessor 来处理的。也就是说 SpringBoot 他启动貌似不需要解析器?

OK,来看下这个方法做了什么事情:

1
2
3
4
5
6
7
8
9
10
protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());

// Detect a LoadTimeWeaver and prepare for weaving, if found in the meantime
// (e.g. through an @Bean method registered by ConfigurationClassPostProcessor)
if (beanFactory.getTempClassLoader() == null && beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) {
beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));
beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
}
}

下面那句话说了,是织入运行时处理器,那么肯定是在第一句话就做了注册的。继续进入观察。 getBeanFactoryPostProcessors() 在目前的状态,是一个空的集合,所以我们现在需要进入看看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
public static void invokeBeanFactoryPostProcessors(
ConfigurableListableBeanFactory beanFactory, List<BeanFactoryPostProcessor> beanFactoryPostProcessors) {

// Invoke BeanDefinitionRegistryPostProcessors first, if any.
Set<String> processedBeans = new HashSet<>();

if (beanFactory instanceof BeanDefinitionRegistry) {
BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
List<BeanFactoryPostProcessor> regularPostProcessors = new ArrayList<>();
List<BeanDefinitionRegistryPostProcessor> registryProcessors = new ArrayList<>();

// 因为我们目前没有任何的 beanFactoryPostProcessors 所以这个循环并不会进入。
for (BeanFactoryPostProcessor postProcessor : beanFactoryPostProcessors) {
if (postProcessor instanceof BeanDefinitionRegistryPostProcessor) {
BeanDefinitionRegistryPostProcessor registryProcessor =
(BeanDefinitionRegistryPostProcessor) postProcessor;
registryProcessor.postProcessBeanDefinitionRegistry(registry);
registryProcessors.add(registryProcessor);
}
else {
regularPostProcessors.add(postProcessor);
}
}

// Do not initialize FactoryBeans here: We need to leave all regular beans
// uninitialized to let the bean factory post-processors apply to them!
// Separate between BeanDefinitionRegistryPostProcessors that implement
// PriorityOrdered, Ordered, and the rest.
// 在这里不初始化 FactoryBeans 从而可以让 beanfactory 的 post-processors 来处理他们
// 因为下面需要排序这些配置 Bean 的执行顺序。
List<BeanDefinitionRegistryPostProcessor> currentRegistryProcessors = new ArrayList<>();

// 开始排序这些Bean的顺序
String[] postProcessorNames =
beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);
for (String ppName : postProcessorNames) {
if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
processedBeans.add(ppName);
}
}
sortPostProcessors(currentRegistryProcessors, beanFactory);
registryProcessors.addAll(currentRegistryProcessors);
// 开始执行这些处理器!
invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry);
currentRegistryProcessors.clear();

// Next, invoke the BeanDefinitionRegistryPostProcessors that implement Ordered.
postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);
for (String ppName : postProcessorNames) {
if (!processedBeans.contains(ppName) && beanFactory.isTypeMatch(ppName, Ordered.class)) {
currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
processedBeans.add(ppName);
}
}
sortPostProcessors(currentRegistryProcessors, beanFactory);
registryProcessors.addAll(currentRegistryProcessors);
// ---> 在这里开始执行配置的装配
invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry);

/*
// 循环所有的 postProcessors 开始调用。目前只有一个 BeanDefinitionRegistryPostProcessor
// 是在 applicationContext 准备工作中加入的
private static void invokeBeanDefinitionRegistryPostProcessors(
Collection<? extends BeanDefinitionRegistryPostProcessor> postProcessors, BeanDefinitionRegistry registry) {

for (BeanDefinitionRegistryPostProcessor postProcessor : postProcessors) {
postProcessor.postProcessBeanDefinitionRegistry(registry);
}
}
*/

currentRegistryProcessors.clear();

/*
下面这些目前可以先不用看了。是用来执行其他 BeanDefinitionRegistryPostProcessors
因为我目前的主要点在于怎么解析我的配置类。
*/
// Finally, invoke all other BeanDefinitionRegistryPostProcessors until no further ones appear.
boolean reiterate = true;
while (reiterate) {
reiterate = false;
postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);
for (String ppName : postProcessorNames) {
if (!processedBeans.contains(ppName)) {
currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
processedBeans.add(ppName);
reiterate = true;
}
}
sortPostProcessors(currentRegistryProcessors, beanFactory);
registryProcessors.addAll(currentRegistryProcessors);
invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry);
currentRegistryProcessors.clear();
}

// Now, invoke the postProcessBeanFactory callback of all processors handled so far.
invokeBeanFactoryPostProcessors(registryProcessors, beanFactory);
invokeBeanFactoryPostProcessors(regularPostProcessors, beanFactory);
}

else {
// Invoke factory processors registered with the context instance.
invokeBeanFactoryPostProcessors(beanFactoryPostProcessors, beanFactory);
}

// Do not initialize FactoryBeans here: We need to leave all regular beans
// uninitialized to let the bean factory post-processors apply to them!
String[] postProcessorNames =
beanFactory.getBeanNamesForType(BeanFactoryPostProcessor.class, true, false);

// Separate between BeanFactoryPostProcessors that implement PriorityOrdered,
// Ordered, and the rest.
List<BeanFactoryPostProcessor> priorityOrderedPostProcessors = new ArrayList<>();
List<String> orderedPostProcessorNames = new ArrayList<>();
List<String> nonOrderedPostProcessorNames = new ArrayList<>();
for (String ppName : postProcessorNames) {
if (processedBeans.contains(ppName)) {
// skip - already processed in first phase above
}
else if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
priorityOrderedPostProcessors.add(beanFactory.getBean(ppName, BeanFactoryPostProcessor.class));
}
else if (beanFactory.isTypeMatch(ppName, Ordered.class)) {
orderedPostProcessorNames.add(ppName);
}
else {
nonOrderedPostProcessorNames.add(ppName);
}
}

// First, invoke the BeanFactoryPostProcessors that implement PriorityOrdered.
sortPostProcessors(priorityOrderedPostProcessors, beanFactory);
invokeBeanFactoryPostProcessors(priorityOrderedPostProcessors, beanFactory);

// Next, invoke the BeanFactoryPostProcessors that implement Ordered.
List<BeanFactoryPostProcessor> orderedPostProcessors = new ArrayList<>(orderedPostProcessorNames.size());
for (String postProcessorName : orderedPostProcessorNames) {
orderedPostProcessors.add(beanFactory.getBean(postProcessorName, BeanFactoryPostProcessor.class));
}
sortPostProcessors(orderedPostProcessors, beanFactory);
invokeBeanFactoryPostProcessors(orderedPostProcessors, beanFactory);

// Finally, invoke all other BeanFactoryPostProcessors.
List<BeanFactoryPostProcessor> nonOrderedPostProcessors = new ArrayList<>(nonOrderedPostProcessorNames.size());
for (String postProcessorName : nonOrderedPostProcessorNames) {
nonOrderedPostProcessors.add(beanFactory.getBean(postProcessorName, BeanFactoryPostProcessor.class));
}
invokeBeanFactoryPostProcessors(nonOrderedPostProcessors, beanFactory);

// Clear cached merged bean definitions since the post-processors might have
// modified the original metadata, e.g. replacing placeholders in values...
beanFactory.clearMetadataCache();
}

好我现在要在装配那部分开始进入了,因为 Spring 在开始的时候调用了 ConfigurationClassPostProcessorpostProcessBeanDefinitionRegistry 方法,现在要看看这个方法做了什么事情。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
int registryId = System.identityHashCode(registry);
if (this.registriesPostProcessed.contains(registryId)) {
throw new IllegalStateException(
"postProcessBeanDefinitionRegistry already called on this post-processor against " + registry);
}
if (this.factoriesPostProcessed.contains(registryId)) {
throw new IllegalStateException(
"postProcessBeanFactory already called on this post-processor against " + registry);
}
this.registriesPostProcessed.add(registryId);

// 开始解析 ConfigBean 的内容
processConfigBeanDefinitions(registry);
}

public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
List<BeanDefinitionHolder> configCandidates = new ArrayList<>();
String[] candidateNames = registry.getBeanDefinitionNames();

// 开始拿 registry 中的所有 Bean 一顿循环。
for (String beanName : candidateNames) {
BeanDefinition beanDef = registry.getBeanDefinition(beanName);
if (beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE) != null) {
if (logger.isDebugEnabled()) {
logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);
}
}
// 如果 Bean 定义上有 @Configuration 加入 configCandidates 集合中
else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
}
}

// Return immediately if no @Configuration classes were found
if (configCandidates.isEmpty()) {
return;
}

// 排序,但是目前这一步并不需要
configCandidates.sort((bd1, bd2) -> {
int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition());
int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition());
return Integer.compare(i1, i2);
});

// Detect any custom bean name generation strategy supplied through the enclosing application context
SingletonBeanRegistry sbr = null;
if (registry instanceof SingletonBeanRegistry) {
sbr = (SingletonBeanRegistry) registry;
if (!this.localBeanNameGeneratorSet) {
BeanNameGenerator generator = (BeanNameGenerator) sbr.getSingleton(
AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR);
if (generator != null) {
this.componentScanBeanNameGenerator = generator;
this.importBeanNameGenerator = generator;
}
}
}

if (this.environment == null) {
this.environment = new StandardEnvironment();
}

// 开始使用 ConfigurationClassParser 来解析配置类
ConfigurationClassParser parser = new ConfigurationClassParser(
this.metadataReaderFactory, this.problemReporter, this.environment,
this.resourceLoader, this.componentScanBeanNameGenerator, registry);

Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());
do {
parser.parse(candidates);
parser.validate();

Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
configClasses.removeAll(alreadyParsed);

// 使用 ConfigurationClassBeanDefinitionReader 来解析配置类
// 这一步就跟 xml reader 一样,解析里面各个标签
if (this.reader == null) {
this.reader = new ConfigurationClassBeanDefinitionReader(
registry, this.sourceExtractor, this.resourceLoader, this.environment,
this.importBeanNameGenerator, parser.getImportRegistry());
}
this.reader.loadBeanDefinitions(configClasses);
alreadyParsed.addAll(configClasses);

candidates.clear();
if (registry.getBeanDefinitionCount() > candidateNames.length) {
String[] newCandidateNames = registry.getBeanDefinitionNames();
Set<String> oldCandidateNames = new HashSet<>(Arrays.asList(candidateNames));
Set<String> alreadyParsedClasses = new HashSet<>();
for (ConfigurationClass configurationClass : alreadyParsed) {
alreadyParsedClasses.add(configurationClass.getMetadata().getClassName());
}
for (String candidateName : newCandidateNames) {
if (!oldCandidateNames.contains(candidateName)) {
BeanDefinition bd = registry.getBeanDefinition(candidateName);
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, this.metadataReaderFactory) &&
!alreadyParsedClasses.contains(bd.getBeanClassName())) {
candidates.add(new BeanDefinitionHolder(bd, candidateName));
}
}
}
candidateNames = newCandidateNames;
}
}
while (!candidates.isEmpty());

// Register the ImportRegistry as a bean in order to support ImportAware @Configuration classes
if (sbr != null && !sbr.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) {
sbr.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry());
}

if (this.metadataReaderFactory instanceof CachingMetadataReaderFactory) {
// Clear cache in externally provided MetadataReaderFactory; this is a no-op
// for a shared cache since it'll be cleared by the ApplicationContext.
((CachingMetadataReaderFactory) this.metadataReaderFactory).clearCache();
}
}

首先看看 ConfigurationClassParser 是怎么解析配置的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
ConfigurationClassParser#parse
public void parse(Set<BeanDefinitionHolder> configCandidates) {
for (BeanDefinitionHolder holder : configCandidates) {
BeanDefinition bd = holder.getBeanDefinition();
try {
if (bd instanceof AnnotatedBeanDefinition) {
parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
}
else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) {
parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName());
}
else {
// 直接进入到这里解析(其实上面看起来就是强转之前的判断)
parse(bd.getBeanClassName(), holder.getBeanName());
}
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex);
}
}

this.deferredImportSelectorHandler.process();
}

parse 里边先根据环境,简单封装了配置类的信息以及其他的相关信息 MetadataReader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
protected final void parse(@Nullable String className, String beanName) throws IOException {
Assert.notNull(className, "No bean class name for configuration class bean definition");
MetadataReader reader = this.metadataReaderFactory.getMetadataReader(className);
processConfigurationClass(new ConfigurationClass(reader, beanName));
}
protected void processConfigurationClass(ConfigurationClass configClass) throws IOException {
if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {
return;
}

ConfigurationClass existingClass = this.configurationClasses.get(configClass);
if (existingClass != null) {
if (configClass.isImported()) {
if (existingClass.isImported()) {
existingClass.mergeImportedBy(configClass);
}
// Otherwise ignore new imported config class; existing non-imported class overrides it.
return;
}
else {
// Explicit bean definition found, probably replacing an import.
// Let's remove the old one and go with the new one.
this.configurationClasses.remove(configClass);
this.knownSuperclasses.values().removeIf(configClass::equals);
}
}

// 循环解析到父级 但是我们现在
SourceClass sourceClass = asSourceClass(configClass);
do {
// 这里面解析了 @ComponentScan @Import 等注解,见下面,如果有父级,会返回父级,否则返回NULL
sourceClass = doProcessConfigurationClass(configClass, sourceClass);
}
while (sourceClass != null);

this.configurationClasses.put(configClass, configClass);
}
protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)
throws IOException {

if (configClass.getMetadata().isAnnotated(Component.class.getName())) {
// 解析内部类
processMemberClasses(configClass, sourceClass);
}

// 解析配置类(SpringBoot 熟悉的写配置自动提示那种配置)
for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), PropertySources.class,
org.springframework.context.annotation.PropertySource.class)) {
if (this.environment instanceof ConfigurableEnvironment) {
processPropertySource(propertySource);
}
else {
logger.info("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +
"]. Reason: Environment must implement ConfigurableEnvironment");
}
}

// 解析 @ComponentScan 这块打算后面再看了
Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
if (!componentScans.isEmpty() &&
!this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
for (AnnotationAttributes componentScan : componentScans) {
// The config class is annotated with @ComponentScan -> perform the scan immediately
Set<BeanDefinitionHolder> scannedBeanDefinitions =
this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
// Check the set of scanned definitions for any further config classes and parse recursively if needed
for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
if (bdCand == null) {
bdCand = holder.getBeanDefinition();
}
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
parse(bdCand.getBeanClassName(), holder.getBeanName());
}
}
}
}

// 解析 @Import 注解
processImports(configClass, sourceClass, getImports(sourceClass), true);

// 解析 @ImportResource 注解
AnnotationAttributes importResource =
AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class);
if (importResource != null) {
String[] resources = importResource.getStringArray("locations");
Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader");
for (String resource : resources) {
String resolvedResource = this.environment.resolveRequiredPlaceholders(resource);
configClass.addImportedResource(resolvedResource, readerClass);
}
}

// 解析 @Bean ,不过现在只是整理配置了 @Bean 的方法
Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);
for (MethodMetadata methodMetadata : beanMethods) {
configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
}

// Process default methods on interfaces
processInterfaces(configClass, sourceClass);

// Process superclass, if any
if (sourceClass.getMetadata().hasSuperClass()) {
String superclass = sourceClass.getMetadata().getSuperClassName();
if (superclass != null && !superclass.startsWith("java") &&
!this.knownSuperclasses.containsKey(superclass)) {
this.knownSuperclasses.put(superclass, configClass);
// Superclass found, return its annotation metadata and recurse
return sourceClass.getSuperClass();
}
}

// No superclass -> processing is complete
return null;
}

可不可以这么说,在使用 JavaBean 进行配置的时候,Spring 做了一个能够解读里面每一句的意思的类编译器。 接下来,回到 ConfigurationClassPostProcessorprocessConfigBeanDefinitions 里边。调用了 parser.validate(); 主要验证 cglib 生成的配置类。 紧接着开始解析 this.reader.loadBeanDefinitions(configClasses);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
public void loadBeanDefinitions(Set<ConfigurationClass> configurationModel) {
// 我们知道,Configurator是可以增加条件判断的
TrackedConditionEvaluator trackedConditionEvaluator = new TrackedConditionEvaluator();
for (ConfigurationClass configClass : configurationModel) {
// 开始读取 BeanDefinitions
loadBeanDefinitionsForConfigurationClass(configClass, trackedConditionEvaluator);
}
}
private void loadBeanDefinitionsForConfigurationClass(
ConfigurationClass configClass, TrackedConditionEvaluator trackedConditionEvaluator) {

// 如果条线显示,不需要启动此配置,则将其从 BeanFactory 中移除掉
if (trackedConditionEvaluator.shouldSkip(configClass)) {
String beanName = configClass.getBeanName();
if (StringUtils.hasLength(beanName) && this.registry.containsBeanDefinition(beanName)) {
this.registry.removeBeanDefinition(beanName);
}
this.importRegistry.removeImportingClass(configClass.getMetadata().getClassName());
return;
}

if (configClass.isImported()) {
registerBeanDefinitionForImportedConfigurationClass(configClass);
}
// 开始解析每个被 @Bean 修饰的方法
for (BeanMethod beanMethod : configClass.getBeanMethods()) {
loadBeanDefinitionsForBeanMethod(beanMethod);
}

// 读取完成后,读取其他资源
loadBeanDefinitionsFromImportedResources(configClass.getImportedResources());
loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars());
}
private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) {
ConfigurationClass configClass = beanMethod.getConfigurationClass();
MethodMetadata metadata = beanMethod.getMetadata();
String methodName = metadata.getMethodName();

// 继续条件判断,判断是否需要创建 Bean
if (this.conditionEvaluator.shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN)) {
configClass.skippedBeanMethods.add(methodName);
return;
}
if (configClass.skippedBeanMethods.contains(methodName)) {
return;
}

AnnotationAttributes bean = AnnotationConfigUtils.attributesFor(metadata, Bean.class);
Assert.state(bean != null, "No @Bean annotation attributes");

// Consider name and any aliases
List<String> names = new ArrayList<>(Arrays.asList(bean.getStringArray("name")));
// 这一步让我知道了,方法名就是 bean 的名字= =
String beanName = (!names.isEmpty() ? names.remove(0) : methodName);

// Register aliases even when overridden
for (String alias : names) {
this.registry.registerAlias(beanName, alias);
}

// 判断 xml 配置的 override 是否跟 JavaConfig 冲突了
if (isOverriddenByExistingDefinition(beanMethod, beanName)) {
if (beanName.equals(beanMethod.getConfigurationClass().getBeanName())) {
throw new BeanDefinitionStoreException(beanMethod.getConfigurationClass().getResource().getDescription(),
beanName, "Bean name derived from @Bean method '" + beanMethod.getMetadata().getMethodName() +
"' clashes with bean name for containing configuration class; please make those names unique!");
}
return;
}

ConfigurationClassBeanDefinition beanDef = new ConfigurationClassBeanDefinition(configClass, metadata);
beanDef.setResource(configClass.getResource());
beanDef.setSource(this.sourceExtractor.extractSource(metadata, configClass.getResource()));

// 非 static 方法
if (metadata.isStatic()) {
// static @Bean method
if (configClass.getMetadata() instanceof StandardAnnotationMetadata) {
beanDef.setBeanClass(((StandardAnnotationMetadata) configClass.getMetadata()).getIntrospectedClass());
}
else {
beanDef.setBeanClassName(configClass.getMetadata().getClassName());
}
beanDef.setUniqueFactoryMethodName(methodName);
}
else {
// 我的配置被当成 FactoryBean 来处理了
beanDef.setFactoryBeanName(configClass.getBeanName());
beanDef.setUniqueFactoryMethodName(methodName);
}

if (metadata instanceof StandardMethodMetadata) {
beanDef.setResolvedFactoryMethod(((StandardMethodMetadata) metadata).getIntrospectedMethod());
}

beanDef.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR);
beanDef.setAttribute(org.springframework.beans.factory.annotation.RequiredAnnotationBeanPostProcessor.
SKIP_REQUIRED_CHECK_ATTRIBUTE, Boolean.TRUE);

AnnotationConfigUtils.processCommonDefinitionAnnotations(beanDef, metadata);

// 开始解析一系列的属性
Autowire autowire = bean.getEnum("autowire");
if (autowire.isAutowire()) {
beanDef.setAutowireMode(autowire.value());
}

boolean autowireCandidate = bean.getBoolean("autowireCandidate");
if (!autowireCandidate) {
beanDef.setAutowireCandidate(false);
}

String initMethodName = bean.getString("initMethod");
if (StringUtils.hasText(initMethodName)) {
beanDef.setInitMethodName(initMethodName);
}

String destroyMethodName = bean.getString("destroyMethod");
beanDef.setDestroyMethodName(destroyMethodName);

// Consider scoping
ScopedProxyMode proxyMode = ScopedProxyMode.NO;
AnnotationAttributes attributes = AnnotationConfigUtils.attributesFor(metadata, Scope.class);
if (attributes != null) {
beanDef.setScope(attributes.getString("value"));
proxyMode = attributes.getEnum("proxyMode");
if (proxyMode == ScopedProxyMode.DEFAULT) {
proxyMode = ScopedProxyMode.NO;
}
}

// Replace the original bean definition with the target one, if necessary
BeanDefinition beanDefToRegister = beanDef;
if (proxyMode != ScopedProxyMode.NO) {
BeanDefinitionHolder proxyDef = ScopedProxyCreator.createScopedProxy(
new BeanDefinitionHolder(beanDef, beanName), this.registry,
proxyMode == ScopedProxyMode.TARGET_CLASS);
beanDefToRegister = new ConfigurationClassBeanDefinition(
(RootBeanDefinition) proxyDef.getBeanDefinition(), configClass, metadata);
}

if (logger.isTraceEnabled()) {
logger.trace(String.format("Registering bean definition for @Bean method %s.%s()",
configClass.getMetadata().getClassName(), beanName));
}

// 好了,第一篇解析 xml 的时候已经说过了,复用了之前的方法
this.registry.registerBeanDefinition(beanName, beanDefToRegister);
}

好了,现在回到上面刚刚的地方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
/* 
下面这些目前可以先不用看了。是用来执行其他 BeanDefinitionRegistryPostProcessors
因为我目前的主要点在于怎么解析我的配置类。
*/
// 开始运行其他的 BeanDefinitionRegistryPostProcessor
boolean reiterate = true;
while (reiterate) {
reiterate = false;
postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);
for (String ppName : postProcessorNames) {
if (!processedBeans.contains(ppName)) {
currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
processedBeans.add(ppName);
reiterate = true;
}
}
sortPostProcessors(currentRegistryProcessors, beanFactory);
registryProcessors.addAll(currentRegistryProcessors);
invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry);
currentRegistryProcessors.clear();
}

// Now, invoke the postProcessBeanFactory callback of all processors handled so far.
invokeBeanFactoryPostProcessors(registryProcessors, beanFactory);
invokeBeanFactoryPostProcessors(regularPostProcessors, beanFactory);
}

else {
// Invoke factory processors registered with the context instance.
invokeBeanFactoryPostProcessors(beanFactoryPostProcessors, beanFactory);
}

// 然后按照,配置了 PriorityOrdered、配置了 Ordered、未配置顺序的执行顺序来执行这些生命周期回调函数
// 其中使用了 processedBeans 来记录已经执行过的 processor,如果执行过的则会跳过
String[] postProcessorNames =
beanFactory.getBeanNamesForType(BeanFactoryPostProcessor.class, true, false);

// Separate between BeanFactoryPostProcessors that implement PriorityOrdered,
// Ordered, and the rest.
List<BeanFactoryPostProcessor> priorityOrderedPostProcessors = new ArrayList<>();
List<String> orderedPostProcessorNames = new ArrayList<>();
List<String> nonOrderedPostProcessorNames = new ArrayList<>();
for (String ppName : postProcessorNames) {
if (processedBeans.contains(ppName)) {
// skip - already processed in first phase above
}
else if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
priorityOrderedPostProcessors.add(beanFactory.getBean(ppName, BeanFactoryPostProcessor.class));
}
else if (beanFactory.isTypeMatch(ppName, Ordered.class)) {
orderedPostProcessorNames.add(ppName);
}
else {
nonOrderedPostProcessorNames.add(ppName);
}
}

// First, invoke the BeanFactoryPostProcessors that implement PriorityOrdered.
sortPostProcessors(priorityOrderedPostProcessors, beanFactory);
invokeBeanFactoryPostProcessors(priorityOrderedPostProcessors, beanFactory);

// Next, invoke the BeanFactoryPostProcessors that implement Ordered.
List<BeanFactoryPostProcessor> orderedPostProcessors = new ArrayList<>(orderedPostProcessorNames.size());
for (String postProcessorName : orderedPostProcessorNames) {
orderedPostProcessors.add(beanFactory.getBean(postProcessorName, BeanFactoryPostProcessor.class));
}
sortPostProcessors(orderedPostProcessors, beanFactory);
invokeBeanFactoryPostProcessors(orderedPostProcessors, beanFactory);

// Finally, invoke all other BeanFactoryPostProcessors.
List<BeanFactoryPostProcessor> nonOrderedPostProcessors = new ArrayList<>(nonOrderedPostProcessorNames.size());
for (String postProcessorName : nonOrderedPostProcessorNames) {
nonOrderedPostProcessors.add(beanFactory.getBean(postProcessorName, BeanFactoryPostProcessor.class));
}
invokeBeanFactoryPostProcessors(nonOrderedPostProcessors, beanFactory);

// Clear cached merged bean definitions since the post-processors might have
// modified the original metadata, e.g. replacing placeholders in values...
beanFactory.clearMetadataCache();

这串动作做好了以后,我们在 MyBeanConfiguration 里面配置的 MyBean.class,已经被注册到了当前的 BeanFactory 中去了。然后回到这里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
AbstractApplicationContext#refresh():
...
registerBeanPostProcessors(beanFactory);

// Initialize message source for this context.
initMessageSource();

// Initialize event multicaster for this context.
initApplicationEventMulticaster();

// Initialize other special beans in specific context subclasses.
onRefresh();

// Check for listener beans and register them.
registerListeners();

// 在这里实例化我们没有加上 @LazyInit 的对象,也就是 MyBean 在这里进行实例化
finishBeanFactoryInitialization(beanFactory);
...

2.5 初始化配置的Bean对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
// 注册数据转换器
if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME) &&
beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)) {
beanFactory.setConversionService(
beanFactory.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class));
}

// 注册配置文件解析器,解析注入配置文件的那种方式
if (!beanFactory.hasEmbeddedValueResolver()) {
beanFactory.addEmbeddedValueResolver(strVal -> getEnvironment().resolvePlaceholders(strVal));
}

// Initialize LoadTimeWeaverAware beans early to allow for registering their transformers early.
// AOP相关的先跳过
String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false);
for (String weaverAwareName : weaverAwareNames) {
getBean(weaverAwareName);
}

// Stop using the temporary ClassLoader for type matching.
beanFactory.setTempClassLoader(null);

// 缓存BeanDefinition的内容,这时候过后已经停止修改BeanDefinition的内容了
beanFactory.freezeConfiguration();

// 在这里做配置类中所有对象的初始化
beanFactory.preInstantiateSingletons();
}

DefaultListableBeanFactory#preInstantiateSingletons
@Override
public void preInstantiateSingletons() throws BeansException {
if (logger.isTraceEnabled()) {
logger.trace("Pre-instantiating singletons in " + this);
}

// Iterate over a copy to allow for init methods which in turn register new bean definitions.
// While this may not be part of the regular factory bootstrap, it does otherwise work fine.
List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);

// 便利所有非LazyInit的BeanNames,拿到BeanDefinition进行初始化。
for (String beanName : beanNames) {
RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
if (isFactoryBean(beanName)) {
Object bean = getBean(FACTORY_BEAN_PREFIX + beanName);
if (bean instanceof FactoryBean) {
final FactoryBean<?> factory = (FactoryBean<?>) bean;
boolean isEagerInit;
if (System.getSecurityManager() != null && factory instanceof SmartFactoryBean) {
isEagerInit = AccessController.doPrivileged((PrivilegedAction<Boolean>)
((SmartFactoryBean<?>) factory)::isEagerInit,
getAccessControlContext());
}
else {
isEagerInit = (factory instanceof SmartFactoryBean &&
((SmartFactoryBean<?>) factory).isEagerInit());
}
if (isEagerInit) {
getBean(beanName);
}
}
}
else {
// 在这里触发初始化,进去后直接到AbstractBeanFactory#getBean
// 已经很熟悉的函数了,不再细说。
getBean(beanName);
}
}
}

// 触发post-initialization回调函数
for (String beanName : beanNames) {
Object singletonInstance = getSingleton(beanName);
if (singletonInstance instanceof SmartInitializingSingleton) {
final SmartInitializingSingleton smartSingleton = (SmartInitializingSingleton) singletonInstance;
if (System.getSecurityManager() != null) {
AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
smartSingleton.afterSingletonsInstantiated();
return null;
}, getAccessControlContext());
}
else {
smartSingleton.afterSingletonsInstantiated();
}
}
}
}


接下来看看子类怎么创建的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Override
protected final void refreshBeanFactory() throws BeansException {
if (hasBeanFactory()) {
destroyBeans();
closeBeanFactory();
}
try {
DefaultListableBeanFactory beanFactory = createBeanFactory();
beanFactory.setSerializationId(getId());
customizeBeanFactory(beanFactory);
loadBeanDefinitions(beanFactory);
synchronized (this.beanFactoryMonitor) {
this.beanFactory = beanFactory;
}
}
catch (IOException ex) {
throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex);
}
}


@Override
public final ConfigurableListableBeanFactory getBeanFactory() {
synchronized (this.beanFactoryMonitor) {
if (this.beanFactory == null) {
throw new IllegalStateException("BeanFactory not initialized or already closed - " +
"call 'refresh' before accessing beans via the ApplicationContext");
}
return this.beanFactory;
}
}

OK,我们现在看到,首先检测当前是否有工厂实例存在,如果有,直接清理掉里面的数据。然后创建、配置、并且返回回去。在子类看来就只有做这几件事情。 那在当前类的 refreshBeanFactory 方法中,就是着重的看着,是怎么配置并且构建的就 ok 了。 首先看 DefaultListableBeanFactory beanFactory = createBeanFactory(); 这个:

1
2
3
protected DefaultListableBeanFactory createBeanFactory() {
return new DefaultListableBeanFactory(getInternalParentBeanFactory());
}

好吧,过! 然后开始设置 SerializationId 这个也不需要看。 下一个是 customizeBeanFactory(beanFactory);

1
2
3
4
5
6
7
8
protected void customizeBeanFactory(DefaultListableBeanFactory beanFactory) {
if (this.allowBeanDefinitionOverriding != null) {
beanFactory.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
}
if (this.allowCircularReferences != null) {
beanFactory.setAllowCircularReferences(this.allowCircularReferences);
}
}

这是两个自定义的属性,一个是是否允许覆盖 Bean 的定义,一个是是否允许相互依赖,但是这两个 this. 开头的都是 null,如果子类需要修改这两个值的话,则需要在子类调用相对应的 setter。因为没有调用,所以均使用 DefaultListableBeanFactory 默认设置的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory
implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable {

/** Whether to allow re-registration of a different definition with the same name. */
private boolean allowBeanDefinitionOverriding = true;

}

public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory
implements AutowireCapableBeanFactory {

/** Whether to automatically try to resolve circular references between beans. */
private boolean allowCircularReferences = true;

}

然后开始读取配置,然而读取配置的实现也是需要留给子类去实现的:

1
2
3
4
loadBeanDefinitions(beanFactory);

protected abstract void loadBeanDefinitions(DefaultListableBeanFactory beanFactory)
throws BeansException, IOException;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {
// Create a new XmlBeanDefinitionReader for the given BeanFactory.
XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);

// Configure the bean definition reader with this context's
// resource loading environment.
beanDefinitionReader.setEnvironment(this.getEnvironment());
beanDefinitionReader.setResourceLoader(this);
beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));

// Allow a subclass to provide custom initialization of the reader,
// then proceed with actually loading the bean definitions.
initBeanDefinitionReader(beanDefinitionReader);
loadBeanDefinitions(beanDefinitionReader);
}

OK,子类要准备开始读取配置了。不过读取的实现和上一篇大同小异,我就不再重复了。 还记得我们刚刚在哪里吗,我们在 AbstractApplicationContext#refresh 进入了 ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); 这句话,接下来要进一步加工了。

2.3.3 AbstractApplicationContext#prepareBeanFactory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
protected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) {
// Tell the internal bean factory to use the context's class loader etc.
beanFactory.setBeanClassLoader(getClassLoader());
beanFactory.setBeanExpressionResolver(new StandardBeanExpressionResolver(beanFactory.getBeanClassLoader()));
beanFactory.addPropertyEditorRegistrar(new ResourceEditorRegistrar(this, getEnvironment()));

// 注册事件回调
beanFactory.addBeanPostProcessor(new ApplicationContextAwareProcessor(this));
// 实现这些接口的类不允许被注入
// 他们的作用只是用来回调
beanFactory.ignoreDependencyInterface(EnvironmentAware.class);
beanFactory.ignoreDependencyInterface(EmbeddedValueResolverAware.class);
beanFactory.ignoreDependencyInterface(ResourceLoaderAware.class);
beanFactory.ignoreDependencyInterface(ApplicationEventPublisherAware.class);
beanFactory.ignoreDependencyInterface(MessageSourceAware.class);
beanFactory.ignoreDependencyInterface(ApplicationContextAware.class);

// 手动注册一些必要的接口的实现类,比如监听器,可以用于回调
beanFactory.registerResolvableDependency(BeanFactory.class, beanFactory);
beanFactory.registerResolvableDependency(ResourceLoader.class, this);
beanFactory.registerResolvableDependency(ApplicationEventPublisher.class, this);
beanFactory.registerResolvableDependency(ApplicationContext.class, this);

// 内部类调用的事件回调
beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(this));

// 指定类在加载时织入AOP操作
if (beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) {
beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));
// Set a temporary ClassLoader for type matching.
beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
}

// 一些记录环境信息的Bean
if (!beanFactory.containsLocalBean(ENVIRONMENT_BEAN_NAME)) {
beanFactory.registerSingleton(ENVIRONMENT_BEAN_NAME, getEnvironment());
}
if (!beanFactory.containsLocalBean(SYSTEM_PROPERTIES_BEAN_NAME)) {
beanFactory.registerSingleton(SYSTEM_PROPERTIES_BEAN_NAME, getEnvironment().getSystemProperties());
}
if (!beanFactory.containsLocalBean(SYSTEM_ENVIRONMENT_BEAN_NAME)) {
beanFactory.registerSingleton(SYSTEM_ENVIRONMENT_BEAN_NAME, getEnvironment().getSystemEnvironment());
}
}

2.3.4 AbstractApplicationContext#postProcessBeanFactory

这个函数是留给子类实现,这时候 BeanFactory 中的 BeanDefinition 已经全部注册完毕,子类想删除新增或者其他操作都可以继续在这里实现。

2.3.5 AbstractApplicationContext#invokeBeanFactoryPostProcessors

来到这一步,说明 BeanFactory 一切准备就绪,这时候开始调用我们项目中注册到 BeanFactory 中的 BeanFactoryPostProcessors 处理器。 这一步呢,在我们项目中的应用就是,开始自动向权限中心注册当前项目中的用于管理数据的接口,以便项目更新即可分配新的权限。

2.3.6 AbstractApplicationContext#registerBeanPostProcessors

这一步是注册用户配置的实现 BeanPostProcessor 的实现类,用于监听每一个 Bean 实例化的前后所需要做的操作。

2.3.7 其他初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 初始化i18n资源的转换接口
initMessageSource();

// 初始化当前Context的消息广播
initApplicationEventMulticaster();

// 子类可覆写的其他refresh事件
onRefresh();

// 检查、注册监听器
registerListeners();

// 初始化非 lazy-init 的 Bean,如果配置了 lazy-init=false 则
// 配置的 Bean 以及关联的 Bean 会在这个时候被初始化
finishBeanFactoryInitialization(beanFactory);

// 广播事件、清理初始化中的缓存
finishRefresh();

finally {
// 清理初始化过程中用到的 Class 缓存
resetCommonCaches();
}

2.4 初始化配置类配置的Bean

在上面我没有看到配置类配置的 Bean,因为我在 debug 的时候,前几步观察 BeanFactory 的情况,都没看到我注册的 MyBean,后面就跳过了。 然而我错了,我发现在 getBean 的时候,我的 MyBean 他居然已经准备好了,所以肯定是在上面某个环节偷偷给我注册进去了的。 所以我现在需要返回去看看是哪一步进入的。 通过观察,我发现是在 invokeBeanFactoryPostProcessors(beanFactory); 这句话之后发生了 BeanDefinitionMap 的长度变化。 所以现在要看这句话做了什么事情。

其实因为在我想象中,xml 属于一种解析方式,javaConfig 应该跟 xml 平行的属于另外一种,但是貌似完全不是。Spring 是通过 BeanFactoryPostProcessor 来处理的。也就是说 SpringBoot 他启动貌似不需要解析器?

OK,来看下这个方法做了什么事情:

1
2
3
4
5
6
7
8
9
10
protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());

// Detect a LoadTimeWeaver and prepare for weaving, if found in the meantime
// (e.g. through an @Bean method registered by ConfigurationClassPostProcessor)
if (beanFactory.getTempClassLoader() == null && beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) {
beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));
beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
}
}

下面那句话说了,是织入运行时处理器,那么肯定是在第一句话就做了注册的。继续进入观察。 getBeanFactoryPostProcessors() 在目前的状态,是一个空的集合,所以我们现在需要进入看看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
public static void invokeBeanFactoryPostProcessors(
ConfigurableListableBeanFactory beanFactory, List<BeanFactoryPostProcessor> beanFactoryPostProcessors) {

// Invoke BeanDefinitionRegistryPostProcessors first, if any.
Set<String> processedBeans = new HashSet<>();

if (beanFactory instanceof BeanDefinitionRegistry) {
BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
List<BeanFactoryPostProcessor> regularPostProcessors = new ArrayList<>();
List<BeanDefinitionRegistryPostProcessor> registryProcessors = new ArrayList<>();

// 因为我们目前没有任何的 beanFactoryPostProcessors 所以这个循环并不会进入。
for (BeanFactoryPostProcessor postProcessor : beanFactoryPostProcessors) {
if (postProcessor instanceof BeanDefinitionRegistryPostProcessor) {
BeanDefinitionRegistryPostProcessor registryProcessor =
(BeanDefinitionRegistryPostProcessor) postProcessor;
registryProcessor.postProcessBeanDefinitionRegistry(registry);
registryProcessors.add(registryProcessor);
}
else {
regularPostProcessors.add(postProcessor);
}
}

// Do not initialize FactoryBeans here: We need to leave all regular beans
// uninitialized to let the bean factory post-processors apply to them!
// Separate between BeanDefinitionRegistryPostProcessors that implement
// PriorityOrdered, Ordered, and the rest.
// 在这里不初始化 FactoryBeans 从而可以让 beanfactory 的 post-processors 来处理他们
// 因为下面需要排序这些配置 Bean 的执行顺序。
List<BeanDefinitionRegistryPostProcessor> currentRegistryProcessors = new ArrayList<>();

// 开始排序这些Bean的顺序
String[] postProcessorNames =
beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);
for (String ppName : postProcessorNames) {
if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
processedBeans.add(ppName);
}
}
sortPostProcessors(currentRegistryProcessors, beanFactory);
registryProcessors.addAll(currentRegistryProcessors);
// 开始执行这些处理器!
invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry);
currentRegistryProcessors.clear();

// Next, invoke the BeanDefinitionRegistryPostProcessors that implement Ordered.
postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);
for (String ppName : postProcessorNames) {
if (!processedBeans.contains(ppName) && beanFactory.isTypeMatch(ppName, Ordered.class)) {
currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
processedBeans.add(ppName);
}
}
sortPostProcessors(currentRegistryProcessors, beanFactory);
registryProcessors.addAll(currentRegistryProcessors);
// ---> 在这里开始执行配置的装配
invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry);

/*
// 循环所有的 postProcessors 开始调用。目前只有一个 BeanDefinitionRegistryPostProcessor
// 是在 applicationContext 准备工作中加入的
private static void invokeBeanDefinitionRegistryPostProcessors(
Collection<? extends BeanDefinitionRegistryPostProcessor> postProcessors, BeanDefinitionRegistry registry) {

for (BeanDefinitionRegistryPostProcessor postProcessor : postProcessors) {
postProcessor.postProcessBeanDefinitionRegistry(registry);
}
}
*/

currentRegistryProcessors.clear();

/*
下面这些目前可以先不用看了。是用来执行其他 BeanDefinitionRegistryPostProcessors
因为我目前的主要点在于怎么解析我的配置类。
*/
// Finally, invoke all other BeanDefinitionRegistryPostProcessors until no further ones appear.
boolean reiterate = true;
while (reiterate) {
reiterate = false;
postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);
for (String ppName : postProcessorNames) {
if (!processedBeans.contains(ppName)) {
currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
processedBeans.add(ppName);
reiterate = true;
}
}
sortPostProcessors(currentRegistryProcessors, beanFactory);
registryProcessors.addAll(currentRegistryProcessors);
invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry);
currentRegistryProcessors.clear();
}

// Now, invoke the postProcessBeanFactory callback of all processors handled so far.
invokeBeanFactoryPostProcessors(registryProcessors, beanFactory);
invokeBeanFactoryPostProcessors(regularPostProcessors, beanFactory);
}

else {
// Invoke factory processors registered with the context instance.
invokeBeanFactoryPostProcessors(beanFactoryPostProcessors, beanFactory);
}

// Do not initialize FactoryBeans here: We need to leave all regular beans
// uninitialized to let the bean factory post-processors apply to them!
String[] postProcessorNames =
beanFactory.getBeanNamesForType(BeanFactoryPostProcessor.class, true, false);

// Separate between BeanFactoryPostProcessors that implement PriorityOrdered,
// Ordered, and the rest.
List<BeanFactoryPostProcessor> priorityOrderedPostProcessors = new ArrayList<>();
List<String> orderedPostProcessorNames = new ArrayList<>();
List<String> nonOrderedPostProcessorNames = new ArrayList<>();
for (String ppName : postProcessorNames) {
if (processedBeans.contains(ppName)) {
// skip - already processed in first phase above
}
else if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
priorityOrderedPostProcessors.add(beanFactory.getBean(ppName, BeanFactoryPostProcessor.class));
}
else if (beanFactory.isTypeMatch(ppName, Ordered.class)) {
orderedPostProcessorNames.add(ppName);
}
else {
nonOrderedPostProcessorNames.add(ppName);
}
}

// First, invoke the BeanFactoryPostProcessors that implement PriorityOrdered.
sortPostProcessors(priorityOrderedPostProcessors, beanFactory);
invokeBeanFactoryPostProcessors(priorityOrderedPostProcessors, beanFactory);

// Next, invoke the BeanFactoryPostProcessors that implement Ordered.
List<BeanFactoryPostProcessor> orderedPostProcessors = new ArrayList<>(orderedPostProcessorNames.size());
for (String postProcessorName : orderedPostProcessorNames) {
orderedPostProcessors.add(beanFactory.getBean(postProcessorName, BeanFactoryPostProcessor.class));
}
sortPostProcessors(orderedPostProcessors, beanFactory);
invokeBeanFactoryPostProcessors(orderedPostProcessors, beanFactory);

// Finally, invoke all other BeanFactoryPostProcessors.
List<BeanFactoryPostProcessor> nonOrderedPostProcessors = new ArrayList<>(nonOrderedPostProcessorNames.size());
for (String postProcessorName : nonOrderedPostProcessorNames) {
nonOrderedPostProcessors.add(beanFactory.getBean(postProcessorName, BeanFactoryPostProcessor.class));
}
invokeBeanFactoryPostProcessors(nonOrderedPostProcessors, beanFactory);

// Clear cached merged bean definitions since the post-processors might have
// modified the original metadata, e.g. replacing placeholders in values...
beanFactory.clearMetadataCache();
}

好我现在要在装配那部分开始进入了,因为 Spring 在开始的时候调用了 ConfigurationClassPostProcessorpostProcessBeanDefinitionRegistry 方法,现在要看看这个方法做了什么事情。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
int registryId = System.identityHashCode(registry);
if (this.registriesPostProcessed.contains(registryId)) {
throw new IllegalStateException(
"postProcessBeanDefinitionRegistry already called on this post-processor against " + registry);
}
if (this.factoriesPostProcessed.contains(registryId)) {
throw new IllegalStateException(
"postProcessBeanFactory already called on this post-processor against " + registry);
}
this.registriesPostProcessed.add(registryId);

// 开始解析 ConfigBean 的内容
processConfigBeanDefinitions(registry);
}

public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
List<BeanDefinitionHolder> configCandidates = new ArrayList<>();
String[] candidateNames = registry.getBeanDefinitionNames();

// 开始拿 registry 中的所有 Bean 一顿循环。
for (String beanName : candidateNames) {
BeanDefinition beanDef = registry.getBeanDefinition(beanName);
if (beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE) != null) {
if (logger.isDebugEnabled()) {
logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);
}
}
// 如果 Bean 定义上有 @Configuration 加入 configCandidates 集合中
else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
}
}

// Return immediately if no @Configuration classes were found
if (configCandidates.isEmpty()) {
return;
}

// 排序,但是目前这一步并不需要
configCandidates.sort((bd1, bd2) -> {
int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition());
int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition());
return Integer.compare(i1, i2);
});

// Detect any custom bean name generation strategy supplied through the enclosing application context
SingletonBeanRegistry sbr = null;
if (registry instanceof SingletonBeanRegistry) {
sbr = (SingletonBeanRegistry) registry;
if (!this.localBeanNameGeneratorSet) {
BeanNameGenerator generator = (BeanNameGenerator) sbr.getSingleton(
AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR);
if (generator != null) {
this.componentScanBeanNameGenerator = generator;
this.importBeanNameGenerator = generator;
}
}
}

if (this.environment == null) {
this.environment = new StandardEnvironment();
}

// 开始使用 ConfigurationClassParser 来解析配置类
ConfigurationClassParser parser = new ConfigurationClassParser(
this.metadataReaderFactory, this.problemReporter, this.environment,
this.resourceLoader, this.componentScanBeanNameGenerator, registry);

Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());
do {
parser.parse(candidates);
parser.validate();

Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
configClasses.removeAll(alreadyParsed);

// 使用 ConfigurationClassBeanDefinitionReader 来解析配置类
// 这一步就跟 xml reader 一样,解析里面各个标签
if (this.reader == null) {
this.reader = new ConfigurationClassBeanDefinitionReader(
registry, this.sourceExtractor, this.resourceLoader, this.environment,
this.importBeanNameGenerator, parser.getImportRegistry());
}
this.reader.loadBeanDefinitions(configClasses);
alreadyParsed.addAll(configClasses);

candidates.clear();
if (registry.getBeanDefinitionCount() > candidateNames.length) {
String[] newCandidateNames = registry.getBeanDefinitionNames();
Set<String> oldCandidateNames = new HashSet<>(Arrays.asList(candidateNames));
Set<String> alreadyParsedClasses = new HashSet<>();
for (ConfigurationClass configurationClass : alreadyParsed) {
alreadyParsedClasses.add(configurationClass.getMetadata().getClassName());
}
for (String candidateName : newCandidateNames) {
if (!oldCandidateNames.contains(candidateName)) {
BeanDefinition bd = registry.getBeanDefinition(candidateName);
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, this.metadataReaderFactory) &&
!alreadyParsedClasses.contains(bd.getBeanClassName())) {
candidates.add(new BeanDefinitionHolder(bd, candidateName));
}
}
}
candidateNames = newCandidateNames;
}
}
while (!candidates.isEmpty());

// Register the ImportRegistry as a bean in order to support ImportAware @Configuration classes
if (sbr != null && !sbr.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) {
sbr.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry());
}

if (this.metadataReaderFactory instanceof CachingMetadataReaderFactory) {
// Clear cache in externally provided MetadataReaderFactory; this is a no-op
// for a shared cache since it'll be cleared by the ApplicationContext.
((CachingMetadataReaderFactory) this.metadataReaderFactory).clearCache();
}
}

首先看看 ConfigurationClassParser 是怎么解析配置的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
ConfigurationClassParser#parse
public void parse(Set<BeanDefinitionHolder> configCandidates) {
for (BeanDefinitionHolder holder : configCandidates) {
BeanDefinition bd = holder.getBeanDefinition();
try {
if (bd instanceof AnnotatedBeanDefinition) {
parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
}
else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) {
parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName());
}
else {
// 直接进入到这里解析(其实上面看起来就是强转之前的判断)
parse(bd.getBeanClassName(), holder.getBeanName());
}
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex);
}
}

this.deferredImportSelectorHandler.process();
}

parse 里边先根据环境,简单封装了配置类的信息以及其他的相关信息 MetadataReader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
protected final void parse(@Nullable String className, String beanName) throws IOException {
Assert.notNull(className, "No bean class name for configuration class bean definition");
MetadataReader reader = this.metadataReaderFactory.getMetadataReader(className);
processConfigurationClass(new ConfigurationClass(reader, beanName));
}
protected void processConfigurationClass(ConfigurationClass configClass) throws IOException {
if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {
return;
}

ConfigurationClass existingClass = this.configurationClasses.get(configClass);
if (existingClass != null) {
if (configClass.isImported()) {
if (existingClass.isImported()) {
existingClass.mergeImportedBy(configClass);
}
// Otherwise ignore new imported config class; existing non-imported class overrides it.
return;
}
else {
// Explicit bean definition found, probably replacing an import.
// Let's remove the old one and go with the new one.
this.configurationClasses.remove(configClass);
this.knownSuperclasses.values().removeIf(configClass::equals);
}
}

// 循环解析到父级 但是我们现在
SourceClass sourceClass = asSourceClass(configClass);
do {
// 这里面解析了 @ComponentScan @Import 等注解,见下面,如果有父级,会返回父级,否则返回NULL
sourceClass = doProcessConfigurationClass(configClass, sourceClass);
}
while (sourceClass != null);

this.configurationClasses.put(configClass, configClass);
}
protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)
throws IOException {

if (configClass.getMetadata().isAnnotated(Component.class.getName())) {
// 解析内部类
processMemberClasses(configClass, sourceClass);
}

// 解析配置类(SpringBoot 熟悉的写配置自动提示那种配置)
for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), PropertySources.class,
org.springframework.context.annotation.PropertySource.class)) {
if (this.environment instanceof ConfigurableEnvironment) {
processPropertySource(propertySource);
}
else {
logger.info("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +
"]. Reason: Environment must implement ConfigurableEnvironment");
}
}

// 解析 @ComponentScan 这块打算后面再看了
Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
if (!componentScans.isEmpty() &&
!this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
for (AnnotationAttributes componentScan : componentScans) {
// The config class is annotated with @ComponentScan -> perform the scan immediately
Set<BeanDefinitionHolder> scannedBeanDefinitions =
this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
// Check the set of scanned definitions for any further config classes and parse recursively if needed
for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
if (bdCand == null) {
bdCand = holder.getBeanDefinition();
}
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
parse(bdCand.getBeanClassName(), holder.getBeanName());
}
}
}
}

// 解析 @Import 注解
processImports(configClass, sourceClass, getImports(sourceClass), true);

// 解析 @ImportResource 注解
AnnotationAttributes importResource =
AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class);
if (importResource != null) {
String[] resources = importResource.getStringArray("locations");
Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader");
for (String resource : resources) {
String resolvedResource = this.environment.resolveRequiredPlaceholders(resource);
configClass.addImportedResource(resolvedResource, readerClass);
}
}

// 解析 @Bean ,不过现在只是整理配置了 @Bean 的方法
Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);
for (MethodMetadata methodMetadata : beanMethods) {
configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
}

// Process default methods on interfaces
processInterfaces(configClass, sourceClass);

// Process superclass, if any
if (sourceClass.getMetadata().hasSuperClass()) {
String superclass = sourceClass.getMetadata().getSuperClassName();
if (superclass != null && !superclass.startsWith("java") &&
!this.knownSuperclasses.containsKey(superclass)) {
this.knownSuperclasses.put(superclass, configClass);
// Superclass found, return its annotation metadata and recurse
return sourceClass.getSuperClass();
}
}

// No superclass -> processing is complete
return null;
}

可不可以这么说,在使用 JavaBean 进行配置的时候,Spring 做了一个能够解读里面每一句的意思的类编译器。 接下来,回到 ConfigurationClassPostProcessorprocessConfigBeanDefinitions 里边。调用了 parser.validate(); 主要验证 cglib 生成的配置类。 紧接着开始解析 this.reader.loadBeanDefinitions(configClasses);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
public void loadBeanDefinitions(Set<ConfigurationClass> configurationModel) {
// 我们知道,Configurator是可以增加条件判断的
TrackedConditionEvaluator trackedConditionEvaluator = new TrackedConditionEvaluator();
for (ConfigurationClass configClass : configurationModel) {
// 开始读取 BeanDefinitions
loadBeanDefinitionsForConfigurationClass(configClass, trackedConditionEvaluator);
}
}
private void loadBeanDefinitionsForConfigurationClass(
ConfigurationClass configClass, TrackedConditionEvaluator trackedConditionEvaluator) {

// 如果条线显示,不需要启动此配置,则将其从 BeanFactory 中移除掉
if (trackedConditionEvaluator.shouldSkip(configClass)) {
String beanName = configClass.getBeanName();
if (StringUtils.hasLength(beanName) && this.registry.containsBeanDefinition(beanName)) {
this.registry.removeBeanDefinition(beanName);
}
this.importRegistry.removeImportingClass(configClass.getMetadata().getClassName());
return;
}

if (configClass.isImported()) {
registerBeanDefinitionForImportedConfigurationClass(configClass);
}
// 开始解析每个被 @Bean 修饰的方法
for (BeanMethod beanMethod : configClass.getBeanMethods()) {
loadBeanDefinitionsForBeanMethod(beanMethod);
}

// 读取完成后,读取其他资源
loadBeanDefinitionsFromImportedResources(configClass.getImportedResources());
loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars());
}
private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) {
ConfigurationClass configClass = beanMethod.getConfigurationClass();
MethodMetadata metadata = beanMethod.getMetadata();
String methodName = metadata.getMethodName();

// 继续条件判断,判断是否需要创建 Bean
if (this.conditionEvaluator.shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN)) {
configClass.skippedBeanMethods.add(methodName);
return;
}
if (configClass.skippedBeanMethods.contains(methodName)) {
return;
}

AnnotationAttributes bean = AnnotationConfigUtils.attributesFor(metadata, Bean.class);
Assert.state(bean != null, "No @Bean annotation attributes");

// Consider name and any aliases
List<String> names = new ArrayList<>(Arrays.asList(bean.getStringArray("name")));
// 这一步让我知道了,方法名就是 bean 的名字= =
String beanName = (!names.isEmpty() ? names.remove(0) : methodName);

// Register aliases even when overridden
for (String alias : names) {
this.registry.registerAlias(beanName, alias);
}

// 判断 xml 配置的 override 是否跟 JavaConfig 冲突了
if (isOverriddenByExistingDefinition(beanMethod, beanName)) {
if (beanName.equals(beanMethod.getConfigurationClass().getBeanName())) {
throw new BeanDefinitionStoreException(beanMethod.getConfigurationClass().getResource().getDescription(),
beanName, "Bean name derived from @Bean method '" + beanMethod.getMetadata().getMethodName() +
"' clashes with bean name for containing configuration class; please make those names unique!");
}
return;
}

ConfigurationClassBeanDefinition beanDef = new ConfigurationClassBeanDefinition(configClass, metadata);
beanDef.setResource(configClass.getResource());
beanDef.setSource(this.sourceExtractor.extractSource(metadata, configClass.getResource()));

// 非 static 方法
if (metadata.isStatic()) {
// static @Bean method
if (configClass.getMetadata() instanceof StandardAnnotationMetadata) {
beanDef.setBeanClass(((StandardAnnotationMetadata) configClass.getMetadata()).getIntrospectedClass());
}
else {
beanDef.setBeanClassName(configClass.getMetadata().getClassName());
}
beanDef.setUniqueFactoryMethodName(methodName);
}
else {
// 我的配置被当成 FactoryBean 来处理了
beanDef.setFactoryBeanName(configClass.getBeanName());
beanDef.setUniqueFactoryMethodName(methodName);
}

if (metadata instanceof StandardMethodMetadata) {
beanDef.setResolvedFactoryMethod(((StandardMethodMetadata) metadata).getIntrospectedMethod());
}

beanDef.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR);
beanDef.setAttribute(org.springframework.beans.factory.annotation.RequiredAnnotationBeanPostProcessor.
SKIP_REQUIRED_CHECK_ATTRIBUTE, Boolean.TRUE);

AnnotationConfigUtils.processCommonDefinitionAnnotations(beanDef, metadata);

// 开始解析一系列的属性
Autowire autowire = bean.getEnum("autowire");
if (autowire.isAutowire()) {
beanDef.setAutowireMode(autowire.value());
}

boolean autowireCandidate = bean.getBoolean("autowireCandidate");
if (!autowireCandidate) {
beanDef.setAutowireCandidate(false);
}

String initMethodName = bean.getString("initMethod");
if (StringUtils.hasText(initMethodName)) {
beanDef.setInitMethodName(initMethodName);
}

String destroyMethodName = bean.getString("destroyMethod");
beanDef.setDestroyMethodName(destroyMethodName);

// Consider scoping
ScopedProxyMode proxyMode = ScopedProxyMode.NO;
AnnotationAttributes attributes = AnnotationConfigUtils.attributesFor(metadata, Scope.class);
if (attributes != null) {
beanDef.setScope(attributes.getString("value"));
proxyMode = attributes.getEnum("proxyMode");
if (proxyMode == ScopedProxyMode.DEFAULT) {
proxyMode = ScopedProxyMode.NO;
}
}

// Replace the original bean definition with the target one, if necessary
BeanDefinition beanDefToRegister = beanDef;
if (proxyMode != ScopedProxyMode.NO) {
BeanDefinitionHolder proxyDef = ScopedProxyCreator.createScopedProxy(
new BeanDefinitionHolder(beanDef, beanName), this.registry,
proxyMode == ScopedProxyMode.TARGET_CLASS);
beanDefToRegister = new ConfigurationClassBeanDefinition(
(RootBeanDefinition) proxyDef.getBeanDefinition(), configClass, metadata);
}

if (logger.isTraceEnabled()) {
logger.trace(String.format("Registering bean definition for @Bean method %s.%s()",
configClass.getMetadata().getClassName(), beanName));
}

// 好了,第一篇解析 xml 的时候已经说过了,复用了之前的方法
this.registry.registerBeanDefinition(beanName, beanDefToRegister);
}

好了,现在回到上面刚刚的地方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
/* 
下面这些目前可以先不用看了。是用来执行其他 BeanDefinitionRegistryPostProcessors
因为我目前的主要点在于怎么解析我的配置类。
*/
// 开始运行其他的 BeanDefinitionRegistryPostProcessor
boolean reiterate = true;
while (reiterate) {
reiterate = false;
postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);
for (String ppName : postProcessorNames) {
if (!processedBeans.contains(ppName)) {
currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
processedBeans.add(ppName);
reiterate = true;
}
}
sortPostProcessors(currentRegistryProcessors, beanFactory);
registryProcessors.addAll(currentRegistryProcessors);
invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry);
currentRegistryProcessors.clear();
}

// Now, invoke the postProcessBeanFactory callback of all processors handled so far.
invokeBeanFactoryPostProcessors(registryProcessors, beanFactory);
invokeBeanFactoryPostProcessors(regularPostProcessors, beanFactory);
}

else {
// Invoke factory processors registered with the context instance.
invokeBeanFactoryPostProcessors(beanFactoryPostProcessors, beanFactory);
}

// 然后按照,配置了 PriorityOrdered、配置了 Ordered、未配置顺序的执行顺序来执行这些生命周期回调函数
// 其中使用了 processedBeans 来记录已经执行过的 processor,如果执行过的则会跳过
String[] postProcessorNames =
beanFactory.getBeanNamesForType(BeanFactoryPostProcessor.class, true, false);

// Separate between BeanFactoryPostProcessors that implement PriorityOrdered,
// Ordered, and the rest.
List<BeanFactoryPostProcessor> priorityOrderedPostProcessors = new ArrayList<>();
List<String> orderedPostProcessorNames = new ArrayList<>();
List<String> nonOrderedPostProcessorNames = new ArrayList<>();
for (String ppName : postProcessorNames) {
if (processedBeans.contains(ppName)) {
// skip - already processed in first phase above
}
else if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
priorityOrderedPostProcessors.add(beanFactory.getBean(ppName, BeanFactoryPostProcessor.class));
}
else if (beanFactory.isTypeMatch(ppName, Ordered.class)) {
orderedPostProcessorNames.add(ppName);
}
else {
nonOrderedPostProcessorNames.add(ppName);
}
}

// First, invoke the BeanFactoryPostProcessors that implement PriorityOrdered.
sortPostProcessors(priorityOrderedPostProcessors, beanFactory);
invokeBeanFactoryPostProcessors(priorityOrderedPostProcessors, beanFactory);

// Next, invoke the BeanFactoryPostProcessors that implement Ordered.
List<BeanFactoryPostProcessor> orderedPostProcessors = new ArrayList<>(orderedPostProcessorNames.size());
for (String postProcessorName : orderedPostProcessorNames) {
orderedPostProcessors.add(beanFactory.getBean(postProcessorName, BeanFactoryPostProcessor.class));
}
sortPostProcessors(orderedPostProcessors, beanFactory);
invokeBeanFactoryPostProcessors(orderedPostProcessors, beanFactory);

// Finally, invoke all other BeanFactoryPostProcessors.
List<BeanFactoryPostProcessor> nonOrderedPostProcessors = new ArrayList<>(nonOrderedPostProcessorNames.size());
for (String postProcessorName : nonOrderedPostProcessorNames) {
nonOrderedPostProcessors.add(beanFactory.getBean(postProcessorName, BeanFactoryPostProcessor.class));
}
invokeBeanFactoryPostProcessors(nonOrderedPostProcessors, beanFactory);

// Clear cached merged bean definitions since the post-processors might have
// modified the original metadata, e.g. replacing placeholders in values...
beanFactory.clearMetadataCache();

这串动作做好了以后,我们在 MyBeanConfiguration 里面配置的 MyBean.class,已经被注册到了当前的 BeanFactory 中去了。然后回到这里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
AbstractApplicationContext#refresh():
...
registerBeanPostProcessors(beanFactory);

// Initialize message source for this context.
initMessageSource();

// Initialize event multicaster for this context.
initApplicationEventMulticaster();

// Initialize other special beans in specific context subclasses.
onRefresh();

// Check for listener beans and register them.
registerListeners();

// 在这里实例化我们没有加上 @LazyInit 的对象,也就是 MyBean 在这里进行实例化
finishBeanFactoryInitialization(beanFactory);
...

2.5 初始化配置的Bean对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
// 注册数据转换器
if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME) &&
beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)) {
beanFactory.setConversionService(
beanFactory.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class));
}

// 注册配置文件解析器,解析注入配置文件的那种方式
if (!beanFactory.hasEmbeddedValueResolver()) {
beanFactory.addEmbeddedValueResolver(strVal -> getEnvironment().resolvePlaceholders(strVal));
}

// Initialize LoadTimeWeaverAware beans early to allow for registering their transformers early.
// AOP相关的先跳过
String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false);
for (String weaverAwareName : weaverAwareNames) {
getBean(weaverAwareName);
}

// Stop using the temporary ClassLoader for type matching.
beanFactory.setTempClassLoader(null);

// 缓存BeanDefinition的内容,这时候过后已经停止修改BeanDefinition的内容了
beanFactory.freezeConfiguration();

// 在这里做配置类中所有对象的初始化
beanFactory.preInstantiateSingletons();
}

DefaultListableBeanFactory#preInstantiateSingletons
@Override
public void preInstantiateSingletons() throws BeansException {
if (logger.isTraceEnabled()) {
logger.trace("Pre-instantiating singletons in " + this);
}

// Iterate over a copy to allow for init methods which in turn register new bean definitions.
// While this may not be part of the regular factory bootstrap, it does otherwise work fine.
List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);

// 便利所有非LazyInit的BeanNames,拿到BeanDefinition进行初始化。
for (String beanName : beanNames) {
RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
if (isFactoryBean(beanName)) {
Object bean = getBean(FACTORY_BEAN_PREFIX + beanName);
if (bean instanceof FactoryBean) {
final FactoryBean<?> factory = (FactoryBean<?>) bean;
boolean isEagerInit;
if (System.getSecurityManager() != null && factory instanceof SmartFactoryBean) {
isEagerInit = AccessController.doPrivileged((PrivilegedAction<Boolean>)
((SmartFactoryBean<?>) factory)::isEagerInit,
getAccessControlContext());
}
else {
isEagerInit = (factory instanceof SmartFactoryBean &&
((SmartFactoryBean<?>) factory).isEagerInit());
}
if (isEagerInit) {
getBean(beanName);
}
}
}
else {
// 在这里触发初始化,进去后直接到AbstractBeanFactory#getBean
// 已经很熟悉的函数了,不再细说。
getBean(beanName);
}
}
}

// 触发post-initialization回调函数
for (String beanName : beanNames) {
Object singletonInstance = getSingleton(beanName);
if (singletonInstance instanceof SmartInitializingSingleton) {
final SmartInitializingSingleton smartSingleton = (SmartInitializingSingleton) singletonInstance;
if (System.getSecurityManager() != null) {
AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
smartSingleton.afterSingletonsInstantiated();
return null;
}, getAccessControlContext());
}
else {
smartSingleton.afterSingletonsInstantiated();
}
}
}
}

从大学就开始折腾黑苹果,迟迟没有做些记录,总觉得差点什么,所以这次趁我需要重装系统的机会,拍了些照片,结合一些文字来做一个教程吧。 阅读按照文章顺序来,不要跳着来,除非我注明让你可以跳过。

OSX系统

不做评价谁好谁差,但是作为 Web工程师 的话,在 osx 系统下的编程体验确实要比 Windows 下好很多,软件也特别干净,从来没有在你工作的时候,突然右下角来个弹窗给你看广告,或者直接把广告就弹在最顶层来影响你的工作。 jdk npm git Dart Flutter 这些程序,在 unix 系的 osx 下也从来没有报错。所以还是推荐使用的。

安装须知

台式机

在我看来台式机安装黑果是比较完美的,能够屏蔽独立显卡,没有 WiFi 的顾虑。而且较高的性能使得能够更加完美的体验 osx 系统。 但是还是有些限制你需要知道:

  1. Intel 芯支持比较完美,如果是 AMD 芯的话,虽然说现在有破解,但是我还是觉得不如自然免驱的好(无测试过,没什么发言权);
  2. 一般比较热门的独立显卡,在国内远景论坛都有驱动的教程,涉及一些代码的注入,我比较懒,所以公司的电脑就拆掉了独显,使用集显来驱动,集显也是免驱的;

笔记本

  1. 笔记本受限比较大,现在笔记本一般都是 集显+独显 来自动切换达到省电的功能的,但是白苹果的笔记本,不是一个独显就是一个集显。所以,两者混合的情况下一般只能驱动集显(除非部分笔记本可以屏蔽集显),显卡问题就是独显基本就废了;
  2. WiFi 常年无解,两个方法解决:
    1. 淘宝购买 usb 接口的免驱网卡,卖家通常会提供使用所需要的驱动和软件;
    2. 更换笔记本内部的网卡,这一步比较专业,加上联想笔记本更换配件是需要刷白名单的,所以通常来说,都会选择第一种方式解决。

安装流程

镜像下载

因为在这篇文章产生的时候最新的 osx 系统是 10.15 catalina。所以我就是用这个系统来做教程吧。 osx_10.15_catalina 下载地址:下载地址

感谢远景论坛大佬的封装,我没有直接放大佬的最终下载地址,因为我还是希望各位能够给去下载,大佬有硬币赚。

镜像已经下好了,大佬也提供了常用的机型配置:

从大学就开始折腾黑苹果,迟迟没有做些记录,总觉得差点什么,所以这次趁我需要重装系统的机会,拍了些照片,结合一些文字来做一个教程吧。 阅读按照文章顺序来,不要跳着来,除非我注明让你可以跳过。

OSX系统

不做评价谁好谁差,但是作为 Web工程师 的话,在 osx 系统下的编程体验确实要比 Windows 下好很多,软件也特别干净,从来没有在你工作的时候,突然右下角来个弹窗给你看广告,或者直接把广告就弹在最顶层来影响你的工作。 jdk npm git Dart Flutter 这些程序,在 unix 系的 osx 下也从来没有报错。所以还是推荐使用的。

安装须知

台式机

在我看来台式机安装黑果是比较完美的,能够屏蔽独立显卡,没有 WiFi 的顾虑。而且较高的性能使得能够更加完美的体验 osx 系统。 但是还是有些限制你需要知道:

  1. Intel 芯支持比较完美,如果是 AMD 芯的话,虽然说现在有破解,但是我还是觉得不如自然免驱的好(无测试过,没什么发言权);
  2. 一般比较热门的独立显卡,在国内远景论坛都有驱动的教程,涉及一些代码的注入,我比较懒,所以公司的电脑就拆掉了独显,使用集显来驱动,集显也是免驱的;

笔记本

  1. 笔记本受限比较大,现在笔记本一般都是 集显+独显 来自动切换达到省电的功能的,但是白苹果的笔记本,不是一个独显就是一个集显。所以,两者混合的情况下一般只能驱动集显(除非部分笔记本可以屏蔽集显),显卡问题就是独显基本就废了;
  2. WiFi 常年无解,两个方法解决:
    1. 淘宝购买 usb 接口的免驱网卡,卖家通常会提供使用所需要的驱动和软件;
    2. 更换笔记本内部的网卡,这一步比较专业,加上联想笔记本更换配件是需要刷白名单的,所以通常来说,都会选择第一种方式解决。

安装流程

镜像下载

因为在这篇文章产生的时候最新的 osx 系统是 10.15 catalina。所以我就是用这个系统来做教程吧。 osx_10.15_catalina 下载地址:下载地址

感谢远景论坛大佬的封装,我没有直接放大佬的最终下载地址,因为我还是希望各位能够给去下载,大佬有硬币赚。

镜像已经下好了,大佬也提供了常用的机型配置:

这一步只是先让你们看,先放一边,我们通常安装的时候不会直接选择合适的驱动,而是先使用通用驱动来安装,他是以最小的通用兼容配置来做的,通常都能够进入安装并且顺利通过。

写入优盘

此时,你需要拥有一个 16G 的优盘!记住记住,一定是 16G 不然他镜像可以刻录但是不会帮你刻录完整,不完整的镜像就是一个无法安装的镜像。(我用了一个 8G 的在这一步卡了好几个小时,最后把公司的师弟杀了祭天了,他出现了警告也不看直接点击 OK 就给我刻录了一个废盘) 首先你需要一个软件:TransMac 当然因为我的习惯,所以我都是在 Windows 系统下写优盘的。 安装,然后右键 -> 以管理员身份运行

会有提示需要购买正版什么的,不过点击 run 可以直接运行试用版,试用版就够用了。 然后找到你的优盘,右键选择 Format Disk –> Format with Disk Image 即可。

漫长的等待,结束了就可以使用这个优盘来安装系统了。

修改BIOS引导

msruefi,只能选择后者。一般新买的电脑或者近几年的电脑都是这个。 简单的识别方法:开机的时候,如果是品牌 Logo 然后就开始转圈圈进入 Windows 系统的话,就是 uefi。 第二就是修改 biossecurity boot,直接关闭就完事了。如果同等级的还有说什么引导什么系统的话,记得把它修改成 other

如果需要双系统

如果不需要双系统安装,直接跳过这一段

双系统指的是 Windows + osx,好像一般初学者都需要这个东西(包括之前的我自己)? 首先首先,Windows 默认的引导分区太小了,如果你不重新安装 Windows 的话,直接安装 osx 后面都会导致任何软件都无法安装。 教程 类似于上面地址里面的教程,但是在创建 efi 的时候,大小给大点,我一般给的是 512m,因为后面我们可能还需要根据不同的机型,在这个分区里面放入驱动。 分好区,先把 Windows 装上,记得留一些硬盘给 osx,我建议是 100G

装好系统后,在硬盘管理那里给 efi 分区随便给个盘符,然后把里面的东西备份一份出来,因为待会需要对他开刀,备份完整不至于你总是需要重新安装系统。

优盘引导开机

为了安装方便我建议将 first boot 设置为上面的优盘。当然使用 F12 来选择也行不过中途需要重启的时候就要留意重新选择了。 选择优盘中的带有 UEFI 选项的,这时候电脑将会读取优盘的引导,进入引导界面。

进入优盘是这个样子的:

一般是在第一项,下面会提示 Boot macOS Install from xxx 后面一般是优盘它默认给的名字。此时,我们 需要按一次 空格键 ,来到下一个页面:

下面是启动选项,一般按下空格需要设定的,大佬已经帮我们写好了,这个时候直接 按下回车键 进入安装软件就可以了。 一串命令后,我们会进来到这个页面:

能够来到这里,说明安装过程十分顺利,接下来进入下一个环节。

硬盘分区

就跟上面一样,我们选择磁盘工具,然后点击 继续,就会进入到磁盘工具的页面,那我们先选择显示所有设备,这时候磁盘都会显示出来。

然后我们选择整个硬盘(如果是单系统的话,双系统的话,你需要选择你要格式化的那个分区)点击上面的抹掉(看我鼠标那个位置):

点击之后会出来这个硬盘设定界面:

  1. 名称,可以自定义;

  2. 格式,新出来的文件系统是 APFS 听说是提速以及增加安全性的,那我就用这个好了,也建议使用这个。强烈不要使用区分大小写的分区格式(敲黑板),因为我使用了大小写区分的硬盘导致有些软件它不支持!!我目前就是因为这个原因重新安装系统的;

  3. 方案选择 GUID 分区图

稍稍等待一番,成功以后,直接左上角关闭磁盘管理工具。我们就可以安装系统了。

安装系统

回到这个界面,选择 安装 macOS


然后就同意协议了,这个不用图都知道要选择什么吧… 同意协议之后就是选择硬盘了,在这个界面,没有意外的话应该可以看到我们刚刚分出来的硬盘分区:

选好硬盘点击安装,接下来进入漫长的等待:

!!!请注意!!!几分钟动一次鼠标,不要让电脑进入睡眠,因为可能因为驱动原因会睡死。 然后系统在安装过程需要一次重启,别担心,如果你在之前已经设置好优盘第一启动了的话,就不用管它,没有的话,需要手动选择优盘引导,进入优盘不用做什么,会继续安装:

跑完基本就可以进入系统的基础设置页面了:

选择什么不用说了吧,选择其他国家也可以,只要看得懂就好…

完成安装

完成安装可以进入桌面了,左上角有个 -> 关于本机

哎哟卧槽,这个显存不对劲啊….. 那现在我们暂时忍一下,使用憋屈的动画效果使用一下 Safari,去下载一个软件:Clover Configurator 安装软件

LaunchPad 打开运行,不出意外的话:

LaunchPad/其他/终端 运行以下命令,输入设置的用户密码就可以了:

1
sudo spctl --master-disable

重新打开 Clover Configurator 来到这个菜单:

我们现在的引导还是优盘,所以先在优盘那一项点击后面的挂载,这时候应该需要输入用户的密码,挂载成功完以后,就可以在 访达 看到 EFI 分区了。

还记得这张图吗:

将原来的

config.plist 备份一份,把这里面的适合自己机型的配置,改名为 config.plist 放在上一层文件夹:

然后重启电脑,进入优盘引导,根据下面字样提示,选择 Boot macOS from 你设置的分区名 即可进入安装好的系统。如果配置兼容的话,重新查看关于本机:

显存正常了。 再尝试音乐,如果声卡也正常,那就完成安装了。

迁入硬盘引导

一切好了,总不能总是插着硬盘来开机吧,我们需要把硬盘的文件替换到磁盘里面去,还是使用 Clover Configurator

一个是硬盘的,一个是优盘的,同时挂载出来。 然后使用 访达 进行操作:

OK,bios 重新设置第一启动是硬盘,然后,如果需要进入 osx 则选择 osx 硬盘回车,需要 Windows 的话就选择 Windows 所在的分区即可。

其他设置

这是我自己觉得比较好的设置,每个人可以按照自己的习惯来。 当然关于原来在 Windows 习惯的 Ctrl + c 这些都需要换成 Command + C 了,Command 在原来的 Win键 上面,我是不建议在系统设置里面替换这两者的按键位置的,毕竟用起来还蛮舒服的。 然后所有软件需要进入设置界面,都已经统一是 Command + , 这个快捷键了。

访达小设置

访达默认设置我感觉是不太好用的,比如点击访达,显示的不是我的电脑的内容,而是最近使用的内容,我特么写代码最近使用的内容都是代码文件,而我们通常不需要在这里去访问这些文件的(都是通过 IDE 工具),所以我希望点击访达进来的时候就是 我的个人文件夹: 打开 访达Command + ,

第二个不好的点是,我去到那个文件夹,要搜索某个文件,默认给我跑整个电脑去搜索,速度慢又没必要,所以:

第三个不好的点是,文件默认居然不排序,还要像垃圾堆一样堆在一起!!改掉设置!: 这个不在 Command + , 里边了,在这里:

开启Retina渲染

参考资料: 黑苹果开启缩放分辨率HiDPi以及字体模糊的调整方法总结 黑苹果开启HiDPI“解决”字体模糊的问题 直接在 其他软件/终端 粘贴下面的命令运行,提示输入用户密码,输入完以后根据提示来走就可以了:

1
sh -c "$(curl -fsSL https://raw.githubusercontent.com/xzhih/one-key-hidpi/master/hidpi.sh)"

重启电脑后安装 RDM 工具来配置分辨率(下载 RDM-2.2.dmg 就可以了 ) 运行,右上角:

选择带有小闪电的就是 HiDPI 的设置(也就是 Retina 效果)

一般根据屏幕原始分辨率来定,有 4K 显示器开 2K 分辨率的话,效果就跟白苹果一样了,2倍的差距,我目前原始分辨率是 1920 * 1080,开图中的分辨率也就凑合凑合过吧

然后在 系统偏好设置 –> 通用 里面开启字体平滑(在最下面,默认也是开启的)

我对默认的平滑程度还是不太喜欢,要开最高的,终端运行以下命令,提示输入用户密码,输入完以后重启即可:

1
defaults -currentHost write -globalDomain AppleFontSmoothing -int 3

来看看效果图: 开启前: 开启后:

结束

后面有空再更更常用软件了。
这一步只是先让你们看,先放一边,我们通常安装的时候不会直接选择合适的驱动,而是先使用通用驱动来安装,他是以最小的通用兼容配置来做的,通常都能够进入安装并且顺利通过。

写入优盘

此时,你需要拥有一个 16G 的优盘!记住记住,一定是 16G 不然他镜像可以刻录但是不会帮你刻录完整,不完整的镜像就是一个无法安装的镜像。(我用了一个 8G 的在这一步卡了好几个小时,最后把公司的师弟杀了祭天了,他出现了警告也不看直接点击 OK 就给我刻录了一个废盘) 首先你需要一个软件:TransMac 当然因为我的习惯,所以我都是在 Windows 系统下写优盘的。 安装,然后右键 -> 以管理员身份运行

从大学就开始折腾黑苹果,迟迟没有做些记录,总觉得差点什么,所以这次趁我需要重装系统的机会,拍了些照片,结合一些文字来做一个教程吧。 阅读按照文章顺序来,不要跳着来,除非我注明让你可以跳过。

OSX系统

不做评价谁好谁差,但是作为 Web工程师 的话,在 osx 系统下的编程体验确实要比 Windows 下好很多,软件也特别干净,从来没有在你工作的时候,突然右下角来个弹窗给你看广告,或者直接把广告就弹在最顶层来影响你的工作。 jdk npm git Dart Flutter 这些程序,在 unix 系的 osx 下也从来没有报错。所以还是推荐使用的。

安装须知

台式机

在我看来台式机安装黑果是比较完美的,能够屏蔽独立显卡,没有 WiFi 的顾虑。而且较高的性能使得能够更加完美的体验 osx 系统。 但是还是有些限制你需要知道:

  1. Intel 芯支持比较完美,如果是 AMD 芯的话,虽然说现在有破解,但是我还是觉得不如自然免驱的好(无测试过,没什么发言权);
  2. 一般比较热门的独立显卡,在国内远景论坛都有驱动的教程,涉及一些代码的注入,我比较懒,所以公司的电脑就拆掉了独显,使用集显来驱动,集显也是免驱的;

笔记本

  1. 笔记本受限比较大,现在笔记本一般都是 集显+独显 来自动切换达到省电的功能的,但是白苹果的笔记本,不是一个独显就是一个集显。所以,两者混合的情况下一般只能驱动集显(除非部分笔记本可以屏蔽集显),显卡问题就是独显基本就废了;
  2. WiFi 常年无解,两个方法解决:
    1. 淘宝购买 usb 接口的免驱网卡,卖家通常会提供使用所需要的驱动和软件;
    2. 更换笔记本内部的网卡,这一步比较专业,加上联想笔记本更换配件是需要刷白名单的,所以通常来说,都会选择第一种方式解决。

安装流程

镜像下载

因为在这篇文章产生的时候最新的 osx 系统是 10.15 catalina。所以我就是用这个系统来做教程吧。 osx_10.15_catalina 下载地址:下载地址

感谢远景论坛大佬的封装,我没有直接放大佬的最终下载地址,因为我还是希望各位能够给去下载,大佬有硬币赚。

镜像已经下好了,大佬也提供了常用的机型配置:

这一步只是先让你们看,先放一边,我们通常安装的时候不会直接选择合适的驱动,而是先使用通用驱动来安装,他是以最小的通用兼容配置来做的,通常都能够进入安装并且顺利通过。

写入优盘

此时,你需要拥有一个 16G 的优盘!记住记住,一定是 16G 不然他镜像可以刻录但是不会帮你刻录完整,不完整的镜像就是一个无法安装的镜像。(我用了一个 8G 的在这一步卡了好几个小时,最后把公司的师弟杀了祭天了,他出现了警告也不看直接点击 OK 就给我刻录了一个废盘) 首先你需要一个软件:TransMac 当然因为我的习惯,所以我都是在 Windows 系统下写优盘的。 安装,然后右键 -> 以管理员身份运行

会有提示需要购买正版什么的,不过点击 run 可以直接运行试用版,试用版就够用了。 然后找到你的优盘,右键选择 Format Disk –> Format with Disk Image 即可。

漫长的等待,结束了就可以使用这个优盘来安装系统了。

修改BIOS引导

msruefi,只能选择后者。一般新买的电脑或者近几年的电脑都是这个。 简单的识别方法:开机的时候,如果是品牌 Logo 然后就开始转圈圈进入 Windows 系统的话,就是 uefi。 第二就是修改 biossecurity boot,直接关闭就完事了。如果同等级的还有说什么引导什么系统的话,记得把它修改成 other

如果需要双系统

如果不需要双系统安装,直接跳过这一段

双系统指的是 Windows + osx,好像一般初学者都需要这个东西(包括之前的我自己)? 首先首先,Windows 默认的引导分区太小了,如果你不重新安装 Windows 的话,直接安装 osx 后面都会导致任何软件都无法安装。 教程 类似于上面地址里面的教程,但是在创建 efi 的时候,大小给大点,我一般给的是 512m,因为后面我们可能还需要根据不同的机型,在这个分区里面放入驱动。 分好区,先把 Windows 装上,记得留一些硬盘给 osx,我建议是 100G

装好系统后,在硬盘管理那里给 efi 分区随便给个盘符,然后把里面的东西备份一份出来,因为待会需要对他开刀,备份完整不至于你总是需要重新安装系统。

优盘引导开机

为了安装方便我建议将 first boot 设置为上面的优盘。当然使用 F12 来选择也行不过中途需要重启的时候就要留意重新选择了。 选择优盘中的带有 UEFI 选项的,这时候电脑将会读取优盘的引导,进入引导界面。

进入优盘是这个样子的:

一般是在第一项,下面会提示 Boot macOS Install from xxx 后面一般是优盘它默认给的名字。此时,我们 需要按一次 空格键 ,来到下一个页面:

下面是启动选项,一般按下空格需要设定的,大佬已经帮我们写好了,这个时候直接 按下回车键 进入安装软件就可以了。 一串命令后,我们会进来到这个页面:

能够来到这里,说明安装过程十分顺利,接下来进入下一个环节。

硬盘分区

就跟上面一样,我们选择磁盘工具,然后点击 继续,就会进入到磁盘工具的页面,那我们先选择显示所有设备,这时候磁盘都会显示出来。

然后我们选择整个硬盘(如果是单系统的话,双系统的话,你需要选择你要格式化的那个分区)点击上面的抹掉(看我鼠标那个位置):

点击之后会出来这个硬盘设定界面:

  1. 名称,可以自定义;

  2. 格式,新出来的文件系统是 APFS 听说是提速以及增加安全性的,那我就用这个好了,也建议使用这个。强烈不要使用区分大小写的分区格式(敲黑板),因为我使用了大小写区分的硬盘导致有些软件它不支持!!我目前就是因为这个原因重新安装系统的;

  3. 方案选择 GUID 分区图

稍稍等待一番,成功以后,直接左上角关闭磁盘管理工具。我们就可以安装系统了。

安装系统

回到这个界面,选择 安装 macOS


然后就同意协议了,这个不用图都知道要选择什么吧… 同意协议之后就是选择硬盘了,在这个界面,没有意外的话应该可以看到我们刚刚分出来的硬盘分区:

选好硬盘点击安装,接下来进入漫长的等待:

!!!请注意!!!几分钟动一次鼠标,不要让电脑进入睡眠,因为可能因为驱动原因会睡死。 然后系统在安装过程需要一次重启,别担心,如果你在之前已经设置好优盘第一启动了的话,就不用管它,没有的话,需要手动选择优盘引导,进入优盘不用做什么,会继续安装:

跑完基本就可以进入系统的基础设置页面了:

选择什么不用说了吧,选择其他国家也可以,只要看得懂就好…

完成安装

完成安装可以进入桌面了,左上角有个 -> 关于本机

哎哟卧槽,这个显存不对劲啊….. 那现在我们暂时忍一下,使用憋屈的动画效果使用一下 Safari,去下载一个软件:Clover Configurator 安装软件

LaunchPad 打开运行,不出意外的话:

LaunchPad/其他/终端 运行以下命令,输入设置的用户密码就可以了:

1
sudo spctl --master-disable

重新打开 Clover Configurator 来到这个菜单:

我们现在的引导还是优盘,所以先在优盘那一项点击后面的挂载,这时候应该需要输入用户的密码,挂载成功完以后,就可以在 访达 看到 EFI 分区了。

还记得这张图吗:

将原来的

config.plist 备份一份,把这里面的适合自己机型的配置,改名为 config.plist 放在上一层文件夹:

然后重启电脑,进入优盘引导,根据下面字样提示,选择 Boot macOS from 你设置的分区名 即可进入安装好的系统。如果配置兼容的话,重新查看关于本机:

显存正常了。 再尝试音乐,如果声卡也正常,那就完成安装了。

迁入硬盘引导

一切好了,总不能总是插着硬盘来开机吧,我们需要把硬盘的文件替换到磁盘里面去,还是使用 Clover Configurator

一个是硬盘的,一个是优盘的,同时挂载出来。 然后使用 访达 进行操作:

OK,bios 重新设置第一启动是硬盘,然后,如果需要进入 osx 则选择 osx 硬盘回车,需要 Windows 的话就选择 Windows 所在的分区即可。

其他设置

这是我自己觉得比较好的设置,每个人可以按照自己的习惯来。 当然关于原来在 Windows 习惯的 Ctrl + c 这些都需要换成 Command + C 了,Command 在原来的 Win键 上面,我是不建议在系统设置里面替换这两者的按键位置的,毕竟用起来还蛮舒服的。 然后所有软件需要进入设置界面,都已经统一是 Command + , 这个快捷键了。

访达小设置

访达默认设置我感觉是不太好用的,比如点击访达,显示的不是我的电脑的内容,而是最近使用的内容,我特么写代码最近使用的内容都是代码文件,而我们通常不需要在这里去访问这些文件的(都是通过 IDE 工具),所以我希望点击访达进来的时候就是 我的个人文件夹: 打开 访达Command + ,

第二个不好的点是,我去到那个文件夹,要搜索某个文件,默认给我跑整个电脑去搜索,速度慢又没必要,所以:

第三个不好的点是,文件默认居然不排序,还要像垃圾堆一样堆在一起!!改掉设置!: 这个不在 Command + , 里边了,在这里:

开启Retina渲染

参考资料: 黑苹果开启缩放分辨率HiDPi以及字体模糊的调整方法总结 黑苹果开启HiDPI“解决”字体模糊的问题 直接在 其他软件/终端 粘贴下面的命令运行,提示输入用户密码,输入完以后根据提示来走就可以了:

1
sh -c "$(curl -fsSL https://raw.githubusercontent.com/xzhih/one-key-hidpi/master/hidpi.sh)"

重启电脑后安装 RDM 工具来配置分辨率(下载 RDM-2.2.dmg 就可以了 ) 运行,右上角:

选择带有小闪电的就是 HiDPI 的设置(也就是 Retina 效果)

一般根据屏幕原始分辨率来定,有 4K 显示器开 2K 分辨率的话,效果就跟白苹果一样了,2倍的差距,我目前原始分辨率是 1920 * 1080,开图中的分辨率也就凑合凑合过吧

然后在 系统偏好设置 –> 通用 里面开启字体平滑(在最下面,默认也是开启的)

我对默认的平滑程度还是不太喜欢,要开最高的,终端运行以下命令,提示输入用户密码,输入完以后重启即可:

1
defaults -currentHost write -globalDomain AppleFontSmoothing -int 3

来看看效果图: 开启前: 开启后:

结束

后面有空再更更常用软件了。

会有提示需要购买正版什么的,不过点击 run 可以直接运行试用版,试用版就够用了。 然后找到你的优盘,右键选择 Format Disk –> Format with Disk Image 即可。

从大学就开始折腾黑苹果,迟迟没有做些记录,总觉得差点什么,所以这次趁我需要重装系统的机会,拍了些照片,结合一些文字来做一个教程吧。 阅读按照文章顺序来,不要跳着来,除非我注明让你可以跳过。

OSX系统

不做评价谁好谁差,但是作为 Web工程师 的话,在 osx 系统下的编程体验确实要比 Windows 下好很多,软件也特别干净,从来没有在你工作的时候,突然右下角来个弹窗给你看广告,或者直接把广告就弹在最顶层来影响你的工作。 jdk npm git Dart Flutter 这些程序,在 unix 系的 osx 下也从来没有报错。所以还是推荐使用的。

安装须知

台式机

在我看来台式机安装黑果是比较完美的,能够屏蔽独立显卡,没有 WiFi 的顾虑。而且较高的性能使得能够更加完美的体验 osx 系统。 但是还是有些限制你需要知道:

  1. Intel 芯支持比较完美,如果是 AMD 芯的话,虽然说现在有破解,但是我还是觉得不如自然免驱的好(无测试过,没什么发言权);
  2. 一般比较热门的独立显卡,在国内远景论坛都有驱动的教程,涉及一些代码的注入,我比较懒,所以公司的电脑就拆掉了独显,使用集显来驱动,集显也是免驱的;

笔记本

  1. 笔记本受限比较大,现在笔记本一般都是 集显+独显 来自动切换达到省电的功能的,但是白苹果的笔记本,不是一个独显就是一个集显。所以,两者混合的情况下一般只能驱动集显(除非部分笔记本可以屏蔽集显),显卡问题就是独显基本就废了;
  2. WiFi 常年无解,两个方法解决:
    1. 淘宝购买 usb 接口的免驱网卡,卖家通常会提供使用所需要的驱动和软件;
    2. 更换笔记本内部的网卡,这一步比较专业,加上联想笔记本更换配件是需要刷白名单的,所以通常来说,都会选择第一种方式解决。

安装流程

镜像下载

因为在这篇文章产生的时候最新的 osx 系统是 10.15 catalina。所以我就是用这个系统来做教程吧。 osx_10.15_catalina 下载地址:下载地址

感谢远景论坛大佬的封装,我没有直接放大佬的最终下载地址,因为我还是希望各位能够给去下载,大佬有硬币赚。

镜像已经下好了,大佬也提供了常用的机型配置:

这一步只是先让你们看,先放一边,我们通常安装的时候不会直接选择合适的驱动,而是先使用通用驱动来安装,他是以最小的通用兼容配置来做的,通常都能够进入安装并且顺利通过。

写入优盘

此时,你需要拥有一个 16G 的优盘!记住记住,一定是 16G 不然他镜像可以刻录但是不会帮你刻录完整,不完整的镜像就是一个无法安装的镜像。(我用了一个 8G 的在这一步卡了好几个小时,最后把公司的师弟杀了祭天了,他出现了警告也不看直接点击 OK 就给我刻录了一个废盘) 首先你需要一个软件:TransMac 当然因为我的习惯,所以我都是在 Windows 系统下写优盘的。 安装,然后右键 -> 以管理员身份运行

会有提示需要购买正版什么的,不过点击 run 可以直接运行试用版,试用版就够用了。 然后找到你的优盘,右键选择 Format Disk –> Format with Disk Image 即可。

漫长的等待,结束了就可以使用这个优盘来安装系统了。

修改BIOS引导

msruefi,只能选择后者。一般新买的电脑或者近几年的电脑都是这个。 简单的识别方法:开机的时候,如果是品牌 Logo 然后就开始转圈圈进入 Windows 系统的话,就是 uefi。 第二就是修改 biossecurity boot,直接关闭就完事了。如果同等级的还有说什么引导什么系统的话,记得把它修改成 other

如果需要双系统

如果不需要双系统安装,直接跳过这一段

双系统指的是 Windows + osx,好像一般初学者都需要这个东西(包括之前的我自己)? 首先首先,Windows 默认的引导分区太小了,如果你不重新安装 Windows 的话,直接安装 osx 后面都会导致任何软件都无法安装。 教程 类似于上面地址里面的教程,但是在创建 efi 的时候,大小给大点,我一般给的是 512m,因为后面我们可能还需要根据不同的机型,在这个分区里面放入驱动。 分好区,先把 Windows 装上,记得留一些硬盘给 osx,我建议是 100G

装好系统后,在硬盘管理那里给 efi 分区随便给个盘符,然后把里面的东西备份一份出来,因为待会需要对他开刀,备份完整不至于你总是需要重新安装系统。

优盘引导开机

为了安装方便我建议将 first boot 设置为上面的优盘。当然使用 F12 来选择也行不过中途需要重启的时候就要留意重新选择了。 选择优盘中的带有 UEFI 选项的,这时候电脑将会读取优盘的引导,进入引导界面。

进入优盘是这个样子的:

一般是在第一项,下面会提示 Boot macOS Install from xxx 后面一般是优盘它默认给的名字。此时,我们 需要按一次 空格键 ,来到下一个页面:

下面是启动选项,一般按下空格需要设定的,大佬已经帮我们写好了,这个时候直接 按下回车键 进入安装软件就可以了。 一串命令后,我们会进来到这个页面:

能够来到这里,说明安装过程十分顺利,接下来进入下一个环节。

硬盘分区

就跟上面一样,我们选择磁盘工具,然后点击 继续,就会进入到磁盘工具的页面,那我们先选择显示所有设备,这时候磁盘都会显示出来。

然后我们选择整个硬盘(如果是单系统的话,双系统的话,你需要选择你要格式化的那个分区)点击上面的抹掉(看我鼠标那个位置):

点击之后会出来这个硬盘设定界面:

  1. 名称,可以自定义;

  2. 格式,新出来的文件系统是 APFS 听说是提速以及增加安全性的,那我就用这个好了,也建议使用这个。强烈不要使用区分大小写的分区格式(敲黑板),因为我使用了大小写区分的硬盘导致有些软件它不支持!!我目前就是因为这个原因重新安装系统的;

  3. 方案选择 GUID 分区图

稍稍等待一番,成功以后,直接左上角关闭磁盘管理工具。我们就可以安装系统了。

安装系统

回到这个界面,选择 安装 macOS


然后就同意协议了,这个不用图都知道要选择什么吧… 同意协议之后就是选择硬盘了,在这个界面,没有意外的话应该可以看到我们刚刚分出来的硬盘分区:

选好硬盘点击安装,接下来进入漫长的等待:

!!!请注意!!!几分钟动一次鼠标,不要让电脑进入睡眠,因为可能因为驱动原因会睡死。 然后系统在安装过程需要一次重启,别担心,如果你在之前已经设置好优盘第一启动了的话,就不用管它,没有的话,需要手动选择优盘引导,进入优盘不用做什么,会继续安装:

跑完基本就可以进入系统的基础设置页面了:

选择什么不用说了吧,选择其他国家也可以,只要看得懂就好…

完成安装

完成安装可以进入桌面了,左上角有个 -> 关于本机

哎哟卧槽,这个显存不对劲啊….. 那现在我们暂时忍一下,使用憋屈的动画效果使用一下 Safari,去下载一个软件:Clover Configurator 安装软件

LaunchPad 打开运行,不出意外的话:

LaunchPad/其他/终端 运行以下命令,输入设置的用户密码就可以了:

1
sudo spctl --master-disable

重新打开 Clover Configurator 来到这个菜单:

我们现在的引导还是优盘,所以先在优盘那一项点击后面的挂载,这时候应该需要输入用户的密码,挂载成功完以后,就可以在 访达 看到 EFI 分区了。

还记得这张图吗:

将原来的

config.plist 备份一份,把这里面的适合自己机型的配置,改名为 config.plist 放在上一层文件夹:

然后重启电脑,进入优盘引导,根据下面字样提示,选择 Boot macOS from 你设置的分区名 即可进入安装好的系统。如果配置兼容的话,重新查看关于本机:

显存正常了。 再尝试音乐,如果声卡也正常,那就完成安装了。

迁入硬盘引导

一切好了,总不能总是插着硬盘来开机吧,我们需要把硬盘的文件替换到磁盘里面去,还是使用 Clover Configurator

一个是硬盘的,一个是优盘的,同时挂载出来。 然后使用 访达 进行操作:

OK,bios 重新设置第一启动是硬盘,然后,如果需要进入 osx 则选择 osx 硬盘回车,需要 Windows 的话就选择 Windows 所在的分区即可。

其他设置

这是我自己觉得比较好的设置,每个人可以按照自己的习惯来。 当然关于原来在 Windows 习惯的 Ctrl + c 这些都需要换成 Command + C 了,Command 在原来的 Win键 上面,我是不建议在系统设置里面替换这两者的按键位置的,毕竟用起来还蛮舒服的。 然后所有软件需要进入设置界面,都已经统一是 Command + , 这个快捷键了。

访达小设置

访达默认设置我感觉是不太好用的,比如点击访达,显示的不是我的电脑的内容,而是最近使用的内容,我特么写代码最近使用的内容都是代码文件,而我们通常不需要在这里去访问这些文件的(都是通过 IDE 工具),所以我希望点击访达进来的时候就是 我的个人文件夹: 打开 访达Command + ,

第二个不好的点是,我去到那个文件夹,要搜索某个文件,默认给我跑整个电脑去搜索,速度慢又没必要,所以:

第三个不好的点是,文件默认居然不排序,还要像垃圾堆一样堆在一起!!改掉设置!: 这个不在 Command + , 里边了,在这里:

开启Retina渲染

参考资料: 黑苹果开启缩放分辨率HiDPi以及字体模糊的调整方法总结 黑苹果开启HiDPI“解决”字体模糊的问题 直接在 其他软件/终端 粘贴下面的命令运行,提示输入用户密码,输入完以后根据提示来走就可以了:

1
sh -c "$(curl -fsSL https://raw.githubusercontent.com/xzhih/one-key-hidpi/master/hidpi.sh)"

重启电脑后安装 RDM 工具来配置分辨率(下载 RDM-2.2.dmg 就可以了 ) 运行,右上角:

选择带有小闪电的就是 HiDPI 的设置(也就是 Retina 效果)

一般根据屏幕原始分辨率来定,有 4K 显示器开 2K 分辨率的话,效果就跟白苹果一样了,2倍的差距,我目前原始分辨率是 1920 * 1080,开图中的分辨率也就凑合凑合过吧

然后在 系统偏好设置 –> 通用 里面开启字体平滑(在最下面,默认也是开启的)

我对默认的平滑程度还是不太喜欢,要开最高的,终端运行以下命令,提示输入用户密码,输入完以后重启即可:

1
defaults -currentHost write -globalDomain AppleFontSmoothing -int 3

来看看效果图: 开启前: 开启后:

结束

后面有空再更更常用软件了。

漫长的等待,结束了就可以使用这个优盘来安装系统了。

修改BIOS引导

msruefi,只能选择后者。一般新买的电脑或者近几年的电脑都是这个。 简单的识别方法:开机的时候,如果是品牌 Logo 然后就开始转圈圈进入 Windows 系统的话,就是 uefi。 第二就是修改 biossecurity boot,直接关闭就完事了。如果同等级的还有说什么引导什么系统的话,记得把它修改成 other

如果需要双系统

如果不需要双系统安装,直接跳过这一段

双系统指的是 Windows + osx,好像一般初学者都需要这个东西(包括之前的我自己)? 首先首先,Windows 默认的引导分区太小了,如果你不重新安装 Windows 的话,直接安装 osx 后面都会导致任何软件都无法安装。 教程 类似于上面地址里面的教程,但是在创建 efi 的时候,大小给大点,我一般给的是 512m,因为后面我们可能还需要根据不同的机型,在这个分区里面放入驱动。 分好区,先把 Windows 装上,记得留一些硬盘给 osx,我建议是 100G

装好系统后,在硬盘管理那里给 efi 分区随便给个盘符,然后把里面的东西备份一份出来,因为待会需要对他开刀,备份完整不至于你总是需要重新安装系统。

优盘引导开机

为了安装方便我建议将 first boot 设置为上面的优盘。当然使用 F12 来选择也行不过中途需要重启的时候就要留意重新选择了。 选择优盘中的带有 UEFI 选项的,这时候电脑将会读取优盘的引导,进入引导界面。

从大学就开始折腾黑苹果,迟迟没有做些记录,总觉得差点什么,所以这次趁我需要重装系统的机会,拍了些照片,结合一些文字来做一个教程吧。 阅读按照文章顺序来,不要跳着来,除非我注明让你可以跳过。

OSX系统

不做评价谁好谁差,但是作为 Web工程师 的话,在 osx 系统下的编程体验确实要比 Windows 下好很多,软件也特别干净,从来没有在你工作的时候,突然右下角来个弹窗给你看广告,或者直接把广告就弹在最顶层来影响你的工作。 jdk npm git Dart Flutter 这些程序,在 unix 系的 osx 下也从来没有报错。所以还是推荐使用的。

安装须知

台式机

在我看来台式机安装黑果是比较完美的,能够屏蔽独立显卡,没有 WiFi 的顾虑。而且较高的性能使得能够更加完美的体验 osx 系统。 但是还是有些限制你需要知道:

  1. Intel 芯支持比较完美,如果是 AMD 芯的话,虽然说现在有破解,但是我还是觉得不如自然免驱的好(无测试过,没什么发言权);
  2. 一般比较热门的独立显卡,在国内远景论坛都有驱动的教程,涉及一些代码的注入,我比较懒,所以公司的电脑就拆掉了独显,使用集显来驱动,集显也是免驱的;

笔记本

  1. 笔记本受限比较大,现在笔记本一般都是 集显+独显 来自动切换达到省电的功能的,但是白苹果的笔记本,不是一个独显就是一个集显。所以,两者混合的情况下一般只能驱动集显(除非部分笔记本可以屏蔽集显),显卡问题就是独显基本就废了;
  2. WiFi 常年无解,两个方法解决:
    1. 淘宝购买 usb 接口的免驱网卡,卖家通常会提供使用所需要的驱动和软件;
    2. 更换笔记本内部的网卡,这一步比较专业,加上联想笔记本更换配件是需要刷白名单的,所以通常来说,都会选择第一种方式解决。

安装流程

镜像下载

因为在这篇文章产生的时候最新的 osx 系统是 10.15 catalina。所以我就是用这个系统来做教程吧。 osx_10.15_catalina 下载地址:下载地址

感谢远景论坛大佬的封装,我没有直接放大佬的最终下载地址,因为我还是希望各位能够给去下载,大佬有硬币赚。

镜像已经下好了,大佬也提供了常用的机型配置:

这一步只是先让你们看,先放一边,我们通常安装的时候不会直接选择合适的驱动,而是先使用通用驱动来安装,他是以最小的通用兼容配置来做的,通常都能够进入安装并且顺利通过。

写入优盘

此时,你需要拥有一个 16G 的优盘!记住记住,一定是 16G 不然他镜像可以刻录但是不会帮你刻录完整,不完整的镜像就是一个无法安装的镜像。(我用了一个 8G 的在这一步卡了好几个小时,最后把公司的师弟杀了祭天了,他出现了警告也不看直接点击 OK 就给我刻录了一个废盘) 首先你需要一个软件:TransMac 当然因为我的习惯,所以我都是在 Windows 系统下写优盘的。 安装,然后右键 -> 以管理员身份运行

会有提示需要购买正版什么的,不过点击 run 可以直接运行试用版,试用版就够用了。 然后找到你的优盘,右键选择 Format Disk –> Format with Disk Image 即可。

漫长的等待,结束了就可以使用这个优盘来安装系统了。

修改BIOS引导

msruefi,只能选择后者。一般新买的电脑或者近几年的电脑都是这个。 简单的识别方法:开机的时候,如果是品牌 Logo 然后就开始转圈圈进入 Windows 系统的话,就是 uefi。 第二就是修改 biossecurity boot,直接关闭就完事了。如果同等级的还有说什么引导什么系统的话,记得把它修改成 other

如果需要双系统

如果不需要双系统安装,直接跳过这一段

双系统指的是 Windows + osx,好像一般初学者都需要这个东西(包括之前的我自己)? 首先首先,Windows 默认的引导分区太小了,如果你不重新安装 Windows 的话,直接安装 osx 后面都会导致任何软件都无法安装。 教程 类似于上面地址里面的教程,但是在创建 efi 的时候,大小给大点,我一般给的是 512m,因为后面我们可能还需要根据不同的机型,在这个分区里面放入驱动。 分好区,先把 Windows 装上,记得留一些硬盘给 osx,我建议是 100G

装好系统后,在硬盘管理那里给 efi 分区随便给个盘符,然后把里面的东西备份一份出来,因为待会需要对他开刀,备份完整不至于你总是需要重新安装系统。

优盘引导开机

为了安装方便我建议将 first boot 设置为上面的优盘。当然使用 F12 来选择也行不过中途需要重启的时候就要留意重新选择了。 选择优盘中的带有 UEFI 选项的,这时候电脑将会读取优盘的引导,进入引导界面。

进入优盘是这个样子的:

一般是在第一项,下面会提示 Boot macOS Install from xxx 后面一般是优盘它默认给的名字。此时,我们 需要按一次 空格键 ,来到下一个页面:

下面是启动选项,一般按下空格需要设定的,大佬已经帮我们写好了,这个时候直接 按下回车键 进入安装软件就可以了。 一串命令后,我们会进来到这个页面:

能够来到这里,说明安装过程十分顺利,接下来进入下一个环节。

硬盘分区

就跟上面一样,我们选择磁盘工具,然后点击 继续,就会进入到磁盘工具的页面,那我们先选择显示所有设备,这时候磁盘都会显示出来。

然后我们选择整个硬盘(如果是单系统的话,双系统的话,你需要选择你要格式化的那个分区)点击上面的抹掉(看我鼠标那个位置):

点击之后会出来这个硬盘设定界面:

  1. 名称,可以自定义;

  2. 格式,新出来的文件系统是 APFS 听说是提速以及增加安全性的,那我就用这个好了,也建议使用这个。强烈不要使用区分大小写的分区格式(敲黑板),因为我使用了大小写区分的硬盘导致有些软件它不支持!!我目前就是因为这个原因重新安装系统的;

  3. 方案选择 GUID 分区图

稍稍等待一番,成功以后,直接左上角关闭磁盘管理工具。我们就可以安装系统了。

安装系统

回到这个界面,选择 安装 macOS


然后就同意协议了,这个不用图都知道要选择什么吧… 同意协议之后就是选择硬盘了,在这个界面,没有意外的话应该可以看到我们刚刚分出来的硬盘分区:

选好硬盘点击安装,接下来进入漫长的等待:

!!!请注意!!!几分钟动一次鼠标,不要让电脑进入睡眠,因为可能因为驱动原因会睡死。 然后系统在安装过程需要一次重启,别担心,如果你在之前已经设置好优盘第一启动了的话,就不用管它,没有的话,需要手动选择优盘引导,进入优盘不用做什么,会继续安装:

跑完基本就可以进入系统的基础设置页面了:

选择什么不用说了吧,选择其他国家也可以,只要看得懂就好…

完成安装

完成安装可以进入桌面了,左上角有个 -> 关于本机

哎哟卧槽,这个显存不对劲啊….. 那现在我们暂时忍一下,使用憋屈的动画效果使用一下 Safari,去下载一个软件:Clover Configurator 安装软件

LaunchPad 打开运行,不出意外的话:

LaunchPad/其他/终端 运行以下命令,输入设置的用户密码就可以了:

1
sudo spctl --master-disable

重新打开 Clover Configurator 来到这个菜单:

我们现在的引导还是优盘,所以先在优盘那一项点击后面的挂载,这时候应该需要输入用户的密码,挂载成功完以后,就可以在 访达 看到 EFI 分区了。

还记得这张图吗:

将原来的

config.plist 备份一份,把这里面的适合自己机型的配置,改名为 config.plist 放在上一层文件夹:

然后重启电脑,进入优盘引导,根据下面字样提示,选择 Boot macOS from 你设置的分区名 即可进入安装好的系统。如果配置兼容的话,重新查看关于本机:

显存正常了。 再尝试音乐,如果声卡也正常,那就完成安装了。

迁入硬盘引导

一切好了,总不能总是插着硬盘来开机吧,我们需要把硬盘的文件替换到磁盘里面去,还是使用 Clover Configurator

一个是硬盘的,一个是优盘的,同时挂载出来。 然后使用 访达 进行操作:

OK,bios 重新设置第一启动是硬盘,然后,如果需要进入 osx 则选择 osx 硬盘回车,需要 Windows 的话就选择 Windows 所在的分区即可。

其他设置

这是我自己觉得比较好的设置,每个人可以按照自己的习惯来。 当然关于原来在 Windows 习惯的 Ctrl + c 这些都需要换成 Command + C 了,Command 在原来的 Win键 上面,我是不建议在系统设置里面替换这两者的按键位置的,毕竟用起来还蛮舒服的。 然后所有软件需要进入设置界面,都已经统一是 Command + , 这个快捷键了。

访达小设置

访达默认设置我感觉是不太好用的,比如点击访达,显示的不是我的电脑的内容,而是最近使用的内容,我特么写代码最近使用的内容都是代码文件,而我们通常不需要在这里去访问这些文件的(都是通过 IDE 工具),所以我希望点击访达进来的时候就是 我的个人文件夹: 打开 访达Command + ,

第二个不好的点是,我去到那个文件夹,要搜索某个文件,默认给我跑整个电脑去搜索,速度慢又没必要,所以:

第三个不好的点是,文件默认居然不排序,还要像垃圾堆一样堆在一起!!改掉设置!: 这个不在 Command + , 里边了,在这里:

开启Retina渲染

参考资料: 黑苹果开启缩放分辨率HiDPi以及字体模糊的调整方法总结 黑苹果开启HiDPI“解决”字体模糊的问题 直接在 其他软件/终端 粘贴下面的命令运行,提示输入用户密码,输入完以后根据提示来走就可以了:

1
sh -c "$(curl -fsSL https://raw.githubusercontent.com/xzhih/one-key-hidpi/master/hidpi.sh)"

重启电脑后安装 RDM 工具来配置分辨率(下载 RDM-2.2.dmg 就可以了 ) 运行,右上角:

选择带有小闪电的就是 HiDPI 的设置(也就是 Retina 效果)

一般根据屏幕原始分辨率来定,有 4K 显示器开 2K 分辨率的话,效果就跟白苹果一样了,2倍的差距,我目前原始分辨率是 1920 * 1080,开图中的分辨率也就凑合凑合过吧

然后在 系统偏好设置 –> 通用 里面开启字体平滑(在最下面,默认也是开启的)

我对默认的平滑程度还是不太喜欢,要开最高的,终端运行以下命令,提示输入用户密码,输入完以后重启即可:

1
defaults -currentHost write -globalDomain AppleFontSmoothing -int 3

来看看效果图: 开启前: 开启后:

结束

后面有空再更更常用软件了。
进入优盘是这个样子的:

从大学就开始折腾黑苹果,迟迟没有做些记录,总觉得差点什么,所以这次趁我需要重装系统的机会,拍了些照片,结合一些文字来做一个教程吧。 阅读按照文章顺序来,不要跳着来,除非我注明让你可以跳过。

OSX系统

不做评价谁好谁差,但是作为 Web工程师 的话,在 osx 系统下的编程体验确实要比 Windows 下好很多,软件也特别干净,从来没有在你工作的时候,突然右下角来个弹窗给你看广告,或者直接把广告就弹在最顶层来影响你的工作。 jdk npm git Dart Flutter 这些程序,在 unix 系的 osx 下也从来没有报错。所以还是推荐使用的。

安装须知

台式机

在我看来台式机安装黑果是比较完美的,能够屏蔽独立显卡,没有 WiFi 的顾虑。而且较高的性能使得能够更加完美的体验 osx 系统。 但是还是有些限制你需要知道:

  1. Intel 芯支持比较完美,如果是 AMD 芯的话,虽然说现在有破解,但是我还是觉得不如自然免驱的好(无测试过,没什么发言权);
  2. 一般比较热门的独立显卡,在国内远景论坛都有驱动的教程,涉及一些代码的注入,我比较懒,所以公司的电脑就拆掉了独显,使用集显来驱动,集显也是免驱的;

笔记本

  1. 笔记本受限比较大,现在笔记本一般都是 集显+独显 来自动切换达到省电的功能的,但是白苹果的笔记本,不是一个独显就是一个集显。所以,两者混合的情况下一般只能驱动集显(除非部分笔记本可以屏蔽集显),显卡问题就是独显基本就废了;
  2. WiFi 常年无解,两个方法解决:
    1. 淘宝购买 usb 接口的免驱网卡,卖家通常会提供使用所需要的驱动和软件;
    2. 更换笔记本内部的网卡,这一步比较专业,加上联想笔记本更换配件是需要刷白名单的,所以通常来说,都会选择第一种方式解决。

安装流程

镜像下载

因为在这篇文章产生的时候最新的 osx 系统是 10.15 catalina。所以我就是用这个系统来做教程吧。 osx_10.15_catalina 下载地址:下载地址

感谢远景论坛大佬的封装,我没有直接放大佬的最终下载地址,因为我还是希望各位能够给去下载,大佬有硬币赚。

镜像已经下好了,大佬也提供了常用的机型配置:

这一步只是先让你们看,先放一边,我们通常安装的时候不会直接选择合适的驱动,而是先使用通用驱动来安装,他是以最小的通用兼容配置来做的,通常都能够进入安装并且顺利通过。

写入优盘

此时,你需要拥有一个 16G 的优盘!记住记住,一定是 16G 不然他镜像可以刻录但是不会帮你刻录完整,不完整的镜像就是一个无法安装的镜像。(我用了一个 8G 的在这一步卡了好几个小时,最后把公司的师弟杀了祭天了,他出现了警告也不看直接点击 OK 就给我刻录了一个废盘) 首先你需要一个软件:TransMac 当然因为我的习惯,所以我都是在 Windows 系统下写优盘的。 安装,然后右键 -> 以管理员身份运行

会有提示需要购买正版什么的,不过点击 run 可以直接运行试用版,试用版就够用了。 然后找到你的优盘,右键选择 Format Disk –> Format with Disk Image 即可。

漫长的等待,结束了就可以使用这个优盘来安装系统了。

修改BIOS引导

msruefi,只能选择后者。一般新买的电脑或者近几年的电脑都是这个。 简单的识别方法:开机的时候,如果是品牌 Logo 然后就开始转圈圈进入 Windows 系统的话,就是 uefi。 第二就是修改 biossecurity boot,直接关闭就完事了。如果同等级的还有说什么引导什么系统的话,记得把它修改成 other

如果需要双系统

如果不需要双系统安装,直接跳过这一段

双系统指的是 Windows + osx,好像一般初学者都需要这个东西(包括之前的我自己)? 首先首先,Windows 默认的引导分区太小了,如果你不重新安装 Windows 的话,直接安装 osx 后面都会导致任何软件都无法安装。 教程 类似于上面地址里面的教程,但是在创建 efi 的时候,大小给大点,我一般给的是 512m,因为后面我们可能还需要根据不同的机型,在这个分区里面放入驱动。 分好区,先把 Windows 装上,记得留一些硬盘给 osx,我建议是 100G

装好系统后,在硬盘管理那里给 efi 分区随便给个盘符,然后把里面的东西备份一份出来,因为待会需要对他开刀,备份完整不至于你总是需要重新安装系统。

优盘引导开机

为了安装方便我建议将 first boot 设置为上面的优盘。当然使用 F12 来选择也行不过中途需要重启的时候就要留意重新选择了。 选择优盘中的带有 UEFI 选项的,这时候电脑将会读取优盘的引导,进入引导界面。

进入优盘是这个样子的:

一般是在第一项,下面会提示 Boot macOS Install from xxx 后面一般是优盘它默认给的名字。此时,我们 需要按一次 空格键 ,来到下一个页面:

下面是启动选项,一般按下空格需要设定的,大佬已经帮我们写好了,这个时候直接 按下回车键 进入安装软件就可以了。 一串命令后,我们会进来到这个页面:

能够来到这里,说明安装过程十分顺利,接下来进入下一个环节。

硬盘分区

就跟上面一样,我们选择磁盘工具,然后点击 继续,就会进入到磁盘工具的页面,那我们先选择显示所有设备,这时候磁盘都会显示出来。

然后我们选择整个硬盘(如果是单系统的话,双系统的话,你需要选择你要格式化的那个分区)点击上面的抹掉(看我鼠标那个位置):

点击之后会出来这个硬盘设定界面:

  1. 名称,可以自定义;

  2. 格式,新出来的文件系统是 APFS 听说是提速以及增加安全性的,那我就用这个好了,也建议使用这个。强烈不要使用区分大小写的分区格式(敲黑板),因为我使用了大小写区分的硬盘导致有些软件它不支持!!我目前就是因为这个原因重新安装系统的;

  3. 方案选择 GUID 分区图

稍稍等待一番,成功以后,直接左上角关闭磁盘管理工具。我们就可以安装系统了。

安装系统

回到这个界面,选择 安装 macOS


然后就同意协议了,这个不用图都知道要选择什么吧… 同意协议之后就是选择硬盘了,在这个界面,没有意外的话应该可以看到我们刚刚分出来的硬盘分区:

选好硬盘点击安装,接下来进入漫长的等待:

!!!请注意!!!几分钟动一次鼠标,不要让电脑进入睡眠,因为可能因为驱动原因会睡死。 然后系统在安装过程需要一次重启,别担心,如果你在之前已经设置好优盘第一启动了的话,就不用管它,没有的话,需要手动选择优盘引导,进入优盘不用做什么,会继续安装:

跑完基本就可以进入系统的基础设置页面了:

选择什么不用说了吧,选择其他国家也可以,只要看得懂就好…

完成安装

完成安装可以进入桌面了,左上角有个 -> 关于本机

哎哟卧槽,这个显存不对劲啊….. 那现在我们暂时忍一下,使用憋屈的动画效果使用一下 Safari,去下载一个软件:Clover Configurator 安装软件

LaunchPad 打开运行,不出意外的话:

LaunchPad/其他/终端 运行以下命令,输入设置的用户密码就可以了:

1
sudo spctl --master-disable

重新打开 Clover Configurator 来到这个菜单:

我们现在的引导还是优盘,所以先在优盘那一项点击后面的挂载,这时候应该需要输入用户的密码,挂载成功完以后,就可以在 访达 看到 EFI 分区了。

还记得这张图吗:

将原来的

config.plist 备份一份,把这里面的适合自己机型的配置,改名为 config.plist 放在上一层文件夹:

然后重启电脑,进入优盘引导,根据下面字样提示,选择 Boot macOS from 你设置的分区名 即可进入安装好的系统。如果配置兼容的话,重新查看关于本机:

显存正常了。 再尝试音乐,如果声卡也正常,那就完成安装了。

迁入硬盘引导

一切好了,总不能总是插着硬盘来开机吧,我们需要把硬盘的文件替换到磁盘里面去,还是使用 Clover Configurator

一个是硬盘的,一个是优盘的,同时挂载出来。 然后使用 访达 进行操作:

OK,bios 重新设置第一启动是硬盘,然后,如果需要进入 osx 则选择 osx 硬盘回车,需要 Windows 的话就选择 Windows 所在的分区即可。

其他设置

这是我自己觉得比较好的设置,每个人可以按照自己的习惯来。 当然关于原来在 Windows 习惯的 Ctrl + c 这些都需要换成 Command + C 了,Command 在原来的 Win键 上面,我是不建议在系统设置里面替换这两者的按键位置的,毕竟用起来还蛮舒服的。 然后所有软件需要进入设置界面,都已经统一是 Command + , 这个快捷键了。

访达小设置

访达默认设置我感觉是不太好用的,比如点击访达,显示的不是我的电脑的内容,而是最近使用的内容,我特么写代码最近使用的内容都是代码文件,而我们通常不需要在这里去访问这些文件的(都是通过 IDE 工具),所以我希望点击访达进来的时候就是 我的个人文件夹: 打开 访达Command + ,

第二个不好的点是,我去到那个文件夹,要搜索某个文件,默认给我跑整个电脑去搜索,速度慢又没必要,所以:

第三个不好的点是,文件默认居然不排序,还要像垃圾堆一样堆在一起!!改掉设置!: 这个不在 Command + , 里边了,在这里:

开启Retina渲染

参考资料: 黑苹果开启缩放分辨率HiDPi以及字体模糊的调整方法总结 黑苹果开启HiDPI“解决”字体模糊的问题 直接在 其他软件/终端 粘贴下面的命令运行,提示输入用户密码,输入完以后根据提示来走就可以了:

1
sh -c "$(curl -fsSL https://raw.githubusercontent.com/xzhih/one-key-hidpi/master/hidpi.sh)"

重启电脑后安装 RDM 工具来配置分辨率(下载 RDM-2.2.dmg 就可以了 ) 运行,右上角:

选择带有小闪电的就是 HiDPI 的设置(也就是 Retina 效果)

一般根据屏幕原始分辨率来定,有 4K 显示器开 2K 分辨率的话,效果就跟白苹果一样了,2倍的差距,我目前原始分辨率是 1920 * 1080,开图中的分辨率也就凑合凑合过吧

然后在 系统偏好设置 –> 通用 里面开启字体平滑(在最下面,默认也是开启的)

我对默认的平滑程度还是不太喜欢,要开最高的,终端运行以下命令,提示输入用户密码,输入完以后重启即可:

1
defaults -currentHost write -globalDomain AppleFontSmoothing -int 3

来看看效果图: 开启前: 开启后:

结束

后面有空再更更常用软件了。
一般是在第一项,下面会提示 Boot macOS Install from xxx 后面一般是优盘它默认给的名字。此时,我们 需要按一次 空格键 ,来到下一个页面:

从大学就开始折腾黑苹果,迟迟没有做些记录,总觉得差点什么,所以这次趁我需要重装系统的机会,拍了些照片,结合一些文字来做一个教程吧。 阅读按照文章顺序来,不要跳着来,除非我注明让你可以跳过。

OSX系统

不做评价谁好谁差,但是作为 Web工程师 的话,在 osx 系统下的编程体验确实要比 Windows 下好很多,软件也特别干净,从来没有在你工作的时候,突然右下角来个弹窗给你看广告,或者直接把广告就弹在最顶层来影响你的工作。 jdk npm git Dart Flutter 这些程序,在 unix 系的 osx 下也从来没有报错。所以还是推荐使用的。

安装须知

台式机

在我看来台式机安装黑果是比较完美的,能够屏蔽独立显卡,没有 WiFi 的顾虑。而且较高的性能使得能够更加完美的体验 osx 系统。 但是还是有些限制你需要知道:

  1. Intel 芯支持比较完美,如果是 AMD 芯的话,虽然说现在有破解,但是我还是觉得不如自然免驱的好(无测试过,没什么发言权);
  2. 一般比较热门的独立显卡,在国内远景论坛都有驱动的教程,涉及一些代码的注入,我比较懒,所以公司的电脑就拆掉了独显,使用集显来驱动,集显也是免驱的;

笔记本

  1. 笔记本受限比较大,现在笔记本一般都是 集显+独显 来自动切换达到省电的功能的,但是白苹果的笔记本,不是一个独显就是一个集显。所以,两者混合的情况下一般只能驱动集显(除非部分笔记本可以屏蔽集显),显卡问题就是独显基本就废了;
  2. WiFi 常年无解,两个方法解决:
    1. 淘宝购买 usb 接口的免驱网卡,卖家通常会提供使用所需要的驱动和软件;
    2. 更换笔记本内部的网卡,这一步比较专业,加上联想笔记本更换配件是需要刷白名单的,所以通常来说,都会选择第一种方式解决。

安装流程

镜像下载

因为在这篇文章产生的时候最新的 osx 系统是 10.15 catalina。所以我就是用这个系统来做教程吧。 osx_10.15_catalina 下载地址:下载地址

感谢远景论坛大佬的封装,我没有直接放大佬的最终下载地址,因为我还是希望各位能够给去下载,大佬有硬币赚。

镜像已经下好了,大佬也提供了常用的机型配置:

这一步只是先让你们看,先放一边,我们通常安装的时候不会直接选择合适的驱动,而是先使用通用驱动来安装,他是以最小的通用兼容配置来做的,通常都能够进入安装并且顺利通过。

写入优盘

此时,你需要拥有一个 16G 的优盘!记住记住,一定是 16G 不然他镜像可以刻录但是不会帮你刻录完整,不完整的镜像就是一个无法安装的镜像。(我用了一个 8G 的在这一步卡了好几个小时,最后把公司的师弟杀了祭天了,他出现了警告也不看直接点击 OK 就给我刻录了一个废盘) 首先你需要一个软件:TransMac 当然因为我的习惯,所以我都是在 Windows 系统下写优盘的。 安装,然后右键 -> 以管理员身份运行

会有提示需要购买正版什么的,不过点击 run 可以直接运行试用版,试用版就够用了。 然后找到你的优盘,右键选择 Format Disk –> Format with Disk Image 即可。

漫长的等待,结束了就可以使用这个优盘来安装系统了。

修改BIOS引导

msruefi,只能选择后者。一般新买的电脑或者近几年的电脑都是这个。 简单的识别方法:开机的时候,如果是品牌 Logo 然后就开始转圈圈进入 Windows 系统的话,就是 uefi。 第二就是修改 biossecurity boot,直接关闭就完事了。如果同等级的还有说什么引导什么系统的话,记得把它修改成 other

如果需要双系统

如果不需要双系统安装,直接跳过这一段

双系统指的是 Windows + osx,好像一般初学者都需要这个东西(包括之前的我自己)? 首先首先,Windows 默认的引导分区太小了,如果你不重新安装 Windows 的话,直接安装 osx 后面都会导致任何软件都无法安装。 教程 类似于上面地址里面的教程,但是在创建 efi 的时候,大小给大点,我一般给的是 512m,因为后面我们可能还需要根据不同的机型,在这个分区里面放入驱动。 分好区,先把 Windows 装上,记得留一些硬盘给 osx,我建议是 100G

装好系统后,在硬盘管理那里给 efi 分区随便给个盘符,然后把里面的东西备份一份出来,因为待会需要对他开刀,备份完整不至于你总是需要重新安装系统。

优盘引导开机

为了安装方便我建议将 first boot 设置为上面的优盘。当然使用 F12 来选择也行不过中途需要重启的时候就要留意重新选择了。 选择优盘中的带有 UEFI 选项的,这时候电脑将会读取优盘的引导,进入引导界面。

进入优盘是这个样子的:

一般是在第一项,下面会提示 Boot macOS Install from xxx 后面一般是优盘它默认给的名字。此时,我们 需要按一次 空格键 ,来到下一个页面:

下面是启动选项,一般按下空格需要设定的,大佬已经帮我们写好了,这个时候直接 按下回车键 进入安装软件就可以了。 一串命令后,我们会进来到这个页面:

能够来到这里,说明安装过程十分顺利,接下来进入下一个环节。

硬盘分区

就跟上面一样,我们选择磁盘工具,然后点击 继续,就会进入到磁盘工具的页面,那我们先选择显示所有设备,这时候磁盘都会显示出来。

然后我们选择整个硬盘(如果是单系统的话,双系统的话,你需要选择你要格式化的那个分区)点击上面的抹掉(看我鼠标那个位置):

点击之后会出来这个硬盘设定界面:

  1. 名称,可以自定义;

  2. 格式,新出来的文件系统是 APFS 听说是提速以及增加安全性的,那我就用这个好了,也建议使用这个。强烈不要使用区分大小写的分区格式(敲黑板),因为我使用了大小写区分的硬盘导致有些软件它不支持!!我目前就是因为这个原因重新安装系统的;

  3. 方案选择 GUID 分区图

稍稍等待一番,成功以后,直接左上角关闭磁盘管理工具。我们就可以安装系统了。

安装系统

回到这个界面,选择 安装 macOS


然后就同意协议了,这个不用图都知道要选择什么吧… 同意协议之后就是选择硬盘了,在这个界面,没有意外的话应该可以看到我们刚刚分出来的硬盘分区:

选好硬盘点击安装,接下来进入漫长的等待:

!!!请注意!!!几分钟动一次鼠标,不要让电脑进入睡眠,因为可能因为驱动原因会睡死。 然后系统在安装过程需要一次重启,别担心,如果你在之前已经设置好优盘第一启动了的话,就不用管它,没有的话,需要手动选择优盘引导,进入优盘不用做什么,会继续安装:

跑完基本就可以进入系统的基础设置页面了:

选择什么不用说了吧,选择其他国家也可以,只要看得懂就好…

完成安装

完成安装可以进入桌面了,左上角有个 -> 关于本机

哎哟卧槽,这个显存不对劲啊….. 那现在我们暂时忍一下,使用憋屈的动画效果使用一下 Safari,去下载一个软件:Clover Configurator 安装软件

LaunchPad 打开运行,不出意外的话:

LaunchPad/其他/终端 运行以下命令,输入设置的用户密码就可以了:

1
sudo spctl --master-disable

重新打开 Clover Configurator 来到这个菜单:

我们现在的引导还是优盘,所以先在优盘那一项点击后面的挂载,这时候应该需要输入用户的密码,挂载成功完以后,就可以在 访达 看到 EFI 分区了。

还记得这张图吗:

将原来的

config.plist 备份一份,把这里面的适合自己机型的配置,改名为 config.plist 放在上一层文件夹:

然后重启电脑,进入优盘引导,根据下面字样提示,选择 Boot macOS from 你设置的分区名 即可进入安装好的系统。如果配置兼容的话,重新查看关于本机:

显存正常了。 再尝试音乐,如果声卡也正常,那就完成安装了。

迁入硬盘引导

一切好了,总不能总是插着硬盘来开机吧,我们需要把硬盘的文件替换到磁盘里面去,还是使用 Clover Configurator

一个是硬盘的,一个是优盘的,同时挂载出来。 然后使用 访达 进行操作:

OK,bios 重新设置第一启动是硬盘,然后,如果需要进入 osx 则选择 osx 硬盘回车,需要 Windows 的话就选择 Windows 所在的分区即可。

其他设置

这是我自己觉得比较好的设置,每个人可以按照自己的习惯来。 当然关于原来在 Windows 习惯的 Ctrl + c 这些都需要换成 Command + C 了,Command 在原来的 Win键 上面,我是不建议在系统设置里面替换这两者的按键位置的,毕竟用起来还蛮舒服的。 然后所有软件需要进入设置界面,都已经统一是 Command + , 这个快捷键了。

访达小设置

访达默认设置我感觉是不太好用的,比如点击访达,显示的不是我的电脑的内容,而是最近使用的内容,我特么写代码最近使用的内容都是代码文件,而我们通常不需要在这里去访问这些文件的(都是通过 IDE 工具),所以我希望点击访达进来的时候就是 我的个人文件夹: 打开 访达Command + ,

第二个不好的点是,我去到那个文件夹,要搜索某个文件,默认给我跑整个电脑去搜索,速度慢又没必要,所以:

第三个不好的点是,文件默认居然不排序,还要像垃圾堆一样堆在一起!!改掉设置!: 这个不在 Command + , 里边了,在这里:

开启Retina渲染

参考资料: 黑苹果开启缩放分辨率HiDPi以及字体模糊的调整方法总结 黑苹果开启HiDPI“解决”字体模糊的问题 直接在 其他软件/终端 粘贴下面的命令运行,提示输入用户密码,输入完以后根据提示来走就可以了:

1
sh -c "$(curl -fsSL https://raw.githubusercontent.com/xzhih/one-key-hidpi/master/hidpi.sh)"

重启电脑后安装 RDM 工具来配置分辨率(下载 RDM-2.2.dmg 就可以了 ) 运行,右上角:

选择带有小闪电的就是 HiDPI 的设置(也就是 Retina 效果)

一般根据屏幕原始分辨率来定,有 4K 显示器开 2K 分辨率的话,效果就跟白苹果一样了,2倍的差距,我目前原始分辨率是 1920 * 1080,开图中的分辨率也就凑合凑合过吧

然后在 系统偏好设置 –> 通用 里面开启字体平滑(在最下面,默认也是开启的)

我对默认的平滑程度还是不太喜欢,要开最高的,终端运行以下命令,提示输入用户密码,输入完以后重启即可:

1
defaults -currentHost write -globalDomain AppleFontSmoothing -int 3

来看看效果图: 开启前: 开启后:

结束

后面有空再更更常用软件了。
下面是启动选项,一般按下空格需要设定的,大佬已经帮我们写好了,这个时候直接 按下回车键 进入安装软件就可以了。 一串命令后,我们会进来到这个页面:

从大学就开始折腾黑苹果,迟迟没有做些记录,总觉得差点什么,所以这次趁我需要重装系统的机会,拍了些照片,结合一些文字来做一个教程吧。 阅读按照文章顺序来,不要跳着来,除非我注明让你可以跳过。

OSX系统

不做评价谁好谁差,但是作为 Web工程师 的话,在 osx 系统下的编程体验确实要比 Windows 下好很多,软件也特别干净,从来没有在你工作的时候,突然右下角来个弹窗给你看广告,或者直接把广告就弹在最顶层来影响你的工作。 jdk npm git Dart Flutter 这些程序,在 unix 系的 osx 下也从来没有报错。所以还是推荐使用的。

安装须知

台式机

在我看来台式机安装黑果是比较完美的,能够屏蔽独立显卡,没有 WiFi 的顾虑。而且较高的性能使得能够更加完美的体验 osx 系统。 但是还是有些限制你需要知道:

  1. Intel 芯支持比较完美,如果是 AMD 芯的话,虽然说现在有破解,但是我还是觉得不如自然免驱的好(无测试过,没什么发言权);
  2. 一般比较热门的独立显卡,在国内远景论坛都有驱动的教程,涉及一些代码的注入,我比较懒,所以公司的电脑就拆掉了独显,使用集显来驱动,集显也是免驱的;

笔记本

  1. 笔记本受限比较大,现在笔记本一般都是 集显+独显 来自动切换达到省电的功能的,但是白苹果的笔记本,不是一个独显就是一个集显。所以,两者混合的情况下一般只能驱动集显(除非部分笔记本可以屏蔽集显),显卡问题就是独显基本就废了;
  2. WiFi 常年无解,两个方法解决:
    1. 淘宝购买 usb 接口的免驱网卡,卖家通常会提供使用所需要的驱动和软件;
    2. 更换笔记本内部的网卡,这一步比较专业,加上联想笔记本更换配件是需要刷白名单的,所以通常来说,都会选择第一种方式解决。

安装流程

镜像下载

因为在这篇文章产生的时候最新的 osx 系统是 10.15 catalina。所以我就是用这个系统来做教程吧。 osx_10.15_catalina 下载地址:下载地址

感谢远景论坛大佬的封装,我没有直接放大佬的最终下载地址,因为我还是希望各位能够给去下载,大佬有硬币赚。

镜像已经下好了,大佬也提供了常用的机型配置:

这一步只是先让你们看,先放一边,我们通常安装的时候不会直接选择合适的驱动,而是先使用通用驱动来安装,他是以最小的通用兼容配置来做的,通常都能够进入安装并且顺利通过。

写入优盘

此时,你需要拥有一个 16G 的优盘!记住记住,一定是 16G 不然他镜像可以刻录但是不会帮你刻录完整,不完整的镜像就是一个无法安装的镜像。(我用了一个 8G 的在这一步卡了好几个小时,最后把公司的师弟杀了祭天了,他出现了警告也不看直接点击 OK 就给我刻录了一个废盘) 首先你需要一个软件:TransMac 当然因为我的习惯,所以我都是在 Windows 系统下写优盘的。 安装,然后右键 -> 以管理员身份运行

会有提示需要购买正版什么的,不过点击 run 可以直接运行试用版,试用版就够用了。 然后找到你的优盘,右键选择 Format Disk –> Format with Disk Image 即可。

漫长的等待,结束了就可以使用这个优盘来安装系统了。

修改BIOS引导

msruefi,只能选择后者。一般新买的电脑或者近几年的电脑都是这个。 简单的识别方法:开机的时候,如果是品牌 Logo 然后就开始转圈圈进入 Windows 系统的话,就是 uefi。 第二就是修改 biossecurity boot,直接关闭就完事了。如果同等级的还有说什么引导什么系统的话,记得把它修改成 other

如果需要双系统

如果不需要双系统安装,直接跳过这一段

双系统指的是 Windows + osx,好像一般初学者都需要这个东西(包括之前的我自己)? 首先首先,Windows 默认的引导分区太小了,如果你不重新安装 Windows 的话,直接安装 osx 后面都会导致任何软件都无法安装。 教程 类似于上面地址里面的教程,但是在创建 efi 的时候,大小给大点,我一般给的是 512m,因为后面我们可能还需要根据不同的机型,在这个分区里面放入驱动。 分好区,先把 Windows 装上,记得留一些硬盘给 osx,我建议是 100G

装好系统后,在硬盘管理那里给 efi 分区随便给个盘符,然后把里面的东西备份一份出来,因为待会需要对他开刀,备份完整不至于你总是需要重新安装系统。

优盘引导开机

为了安装方便我建议将 first boot 设置为上面的优盘。当然使用 F12 来选择也行不过中途需要重启的时候就要留意重新选择了。 选择优盘中的带有 UEFI 选项的,这时候电脑将会读取优盘的引导,进入引导界面。

进入优盘是这个样子的:

一般是在第一项,下面会提示 Boot macOS Install from xxx 后面一般是优盘它默认给的名字。此时,我们 需要按一次 空格键 ,来到下一个页面:

下面是启动选项,一般按下空格需要设定的,大佬已经帮我们写好了,这个时候直接 按下回车键 进入安装软件就可以了。 一串命令后,我们会进来到这个页面:

能够来到这里,说明安装过程十分顺利,接下来进入下一个环节。

硬盘分区

就跟上面一样,我们选择磁盘工具,然后点击 继续,就会进入到磁盘工具的页面,那我们先选择显示所有设备,这时候磁盘都会显示出来。

然后我们选择整个硬盘(如果是单系统的话,双系统的话,你需要选择你要格式化的那个分区)点击上面的抹掉(看我鼠标那个位置):

点击之后会出来这个硬盘设定界面:

  1. 名称,可以自定义;

  2. 格式,新出来的文件系统是 APFS 听说是提速以及增加安全性的,那我就用这个好了,也建议使用这个。强烈不要使用区分大小写的分区格式(敲黑板),因为我使用了大小写区分的硬盘导致有些软件它不支持!!我目前就是因为这个原因重新安装系统的;

  3. 方案选择 GUID 分区图

稍稍等待一番,成功以后,直接左上角关闭磁盘管理工具。我们就可以安装系统了。

安装系统

回到这个界面,选择 安装 macOS


然后就同意协议了,这个不用图都知道要选择什么吧… 同意协议之后就是选择硬盘了,在这个界面,没有意外的话应该可以看到我们刚刚分出来的硬盘分区:

选好硬盘点击安装,接下来进入漫长的等待:

!!!请注意!!!几分钟动一次鼠标,不要让电脑进入睡眠,因为可能因为驱动原因会睡死。 然后系统在安装过程需要一次重启,别担心,如果你在之前已经设置好优盘第一启动了的话,就不用管它,没有的话,需要手动选择优盘引导,进入优盘不用做什么,会继续安装:

跑完基本就可以进入系统的基础设置页面了:

选择什么不用说了吧,选择其他国家也可以,只要看得懂就好…

完成安装

完成安装可以进入桌面了,左上角有个 -> 关于本机

哎哟卧槽,这个显存不对劲啊….. 那现在我们暂时忍一下,使用憋屈的动画效果使用一下 Safari,去下载一个软件:Clover Configurator 安装软件

LaunchPad 打开运行,不出意外的话:

LaunchPad/其他/终端 运行以下命令,输入设置的用户密码就可以了:

1
sudo spctl --master-disable

重新打开 Clover Configurator 来到这个菜单:

我们现在的引导还是优盘,所以先在优盘那一项点击后面的挂载,这时候应该需要输入用户的密码,挂载成功完以后,就可以在 访达 看到 EFI 分区了。

还记得这张图吗:

将原来的

config.plist 备份一份,把这里面的适合自己机型的配置,改名为 config.plist 放在上一层文件夹:

然后重启电脑,进入优盘引导,根据下面字样提示,选择 Boot macOS from 你设置的分区名 即可进入安装好的系统。如果配置兼容的话,重新查看关于本机:

显存正常了。 再尝试音乐,如果声卡也正常,那就完成安装了。

迁入硬盘引导

一切好了,总不能总是插着硬盘来开机吧,我们需要把硬盘的文件替换到磁盘里面去,还是使用 Clover Configurator

一个是硬盘的,一个是优盘的,同时挂载出来。 然后使用 访达 进行操作:

OK,bios 重新设置第一启动是硬盘,然后,如果需要进入 osx 则选择 osx 硬盘回车,需要 Windows 的话就选择 Windows 所在的分区即可。

其他设置

这是我自己觉得比较好的设置,每个人可以按照自己的习惯来。 当然关于原来在 Windows 习惯的 Ctrl + c 这些都需要换成 Command + C 了,Command 在原来的 Win键 上面,我是不建议在系统设置里面替换这两者的按键位置的,毕竟用起来还蛮舒服的。 然后所有软件需要进入设置界面,都已经统一是 Command + , 这个快捷键了。

访达小设置

访达默认设置我感觉是不太好用的,比如点击访达,显示的不是我的电脑的内容,而是最近使用的内容,我特么写代码最近使用的内容都是代码文件,而我们通常不需要在这里去访问这些文件的(都是通过 IDE 工具),所以我希望点击访达进来的时候就是 我的个人文件夹: 打开 访达Command + ,

第二个不好的点是,我去到那个文件夹,要搜索某个文件,默认给我跑整个电脑去搜索,速度慢又没必要,所以:

第三个不好的点是,文件默认居然不排序,还要像垃圾堆一样堆在一起!!改掉设置!: 这个不在 Command + , 里边了,在这里:

开启Retina渲染

参考资料: 黑苹果开启缩放分辨率HiDPi以及字体模糊的调整方法总结 黑苹果开启HiDPI“解决”字体模糊的问题 直接在 其他软件/终端 粘贴下面的命令运行,提示输入用户密码,输入完以后根据提示来走就可以了:

1
sh -c "$(curl -fsSL https://raw.githubusercontent.com/xzhih/one-key-hidpi/master/hidpi.sh)"

重启电脑后安装 RDM 工具来配置分辨率(下载 RDM-2.2.dmg 就可以了 ) 运行,右上角:

选择带有小闪电的就是 HiDPI 的设置(也就是 Retina 效果)

一般根据屏幕原始分辨率来定,有 4K 显示器开 2K 分辨率的话,效果就跟白苹果一样了,2倍的差距,我目前原始分辨率是 1920 * 1080,开图中的分辨率也就凑合凑合过吧

然后在 系统偏好设置 –> 通用 里面开启字体平滑(在最下面,默认也是开启的)

我对默认的平滑程度还是不太喜欢,要开最高的,终端运行以下命令,提示输入用户密码,输入完以后重启即可:

1
defaults -currentHost write -globalDomain AppleFontSmoothing -int 3

来看看效果图: 开启前: 开启后:

结束

后面有空再更更常用软件了。

能够来到这里,说明安装过程十分顺利,接下来进入下一个环节。

硬盘分区

就跟上面一样,我们选择磁盘工具,然后点击 继续,就会进入到磁盘工具的页面,那我们先选择显示所有设备,这时候磁盘都会显示出来。

从大学就开始折腾黑苹果,迟迟没有做些记录,总觉得差点什么,所以这次趁我需要重装系统的机会,拍了些照片,结合一些文字来做一个教程吧。 阅读按照文章顺序来,不要跳着来,除非我注明让你可以跳过。

OSX系统

不做评价谁好谁差,但是作为 Web工程师 的话,在 osx 系统下的编程体验确实要比 Windows 下好很多,软件也特别干净,从来没有在你工作的时候,突然右下角来个弹窗给你看广告,或者直接把广告就弹在最顶层来影响你的工作。 jdk npm git Dart Flutter 这些程序,在 unix 系的 osx 下也从来没有报错。所以还是推荐使用的。

安装须知

台式机

在我看来台式机安装黑果是比较完美的,能够屏蔽独立显卡,没有 WiFi 的顾虑。而且较高的性能使得能够更加完美的体验 osx 系统。 但是还是有些限制你需要知道:

  1. Intel 芯支持比较完美,如果是 AMD 芯的话,虽然说现在有破解,但是我还是觉得不如自然免驱的好(无测试过,没什么发言权);
  2. 一般比较热门的独立显卡,在国内远景论坛都有驱动的教程,涉及一些代码的注入,我比较懒,所以公司的电脑就拆掉了独显,使用集显来驱动,集显也是免驱的;

笔记本

  1. 笔记本受限比较大,现在笔记本一般都是 集显+独显 来自动切换达到省电的功能的,但是白苹果的笔记本,不是一个独显就是一个集显。所以,两者混合的情况下一般只能驱动集显(除非部分笔记本可以屏蔽集显),显卡问题就是独显基本就废了;
  2. WiFi 常年无解,两个方法解决:
    1. 淘宝购买 usb 接口的免驱网卡,卖家通常会提供使用所需要的驱动和软件;
    2. 更换笔记本内部的网卡,这一步比较专业,加上联想笔记本更换配件是需要刷白名单的,所以通常来说,都会选择第一种方式解决。

安装流程

镜像下载

因为在这篇文章产生的时候最新的 osx 系统是 10.15 catalina。所以我就是用这个系统来做教程吧。 osx_10.15_catalina 下载地址:下载地址

感谢远景论坛大佬的封装,我没有直接放大佬的最终下载地址,因为我还是希望各位能够给去下载,大佬有硬币赚。

镜像已经下好了,大佬也提供了常用的机型配置:

这一步只是先让你们看,先放一边,我们通常安装的时候不会直接选择合适的驱动,而是先使用通用驱动来安装,他是以最小的通用兼容配置来做的,通常都能够进入安装并且顺利通过。

写入优盘

此时,你需要拥有一个 16G 的优盘!记住记住,一定是 16G 不然他镜像可以刻录但是不会帮你刻录完整,不完整的镜像就是一个无法安装的镜像。(我用了一个 8G 的在这一步卡了好几个小时,最后把公司的师弟杀了祭天了,他出现了警告也不看直接点击 OK 就给我刻录了一个废盘) 首先你需要一个软件:TransMac 当然因为我的习惯,所以我都是在 Windows 系统下写优盘的。 安装,然后右键 -> 以管理员身份运行

会有提示需要购买正版什么的,不过点击 run 可以直接运行试用版,试用版就够用了。 然后找到你的优盘,右键选择 Format Disk –> Format with Disk Image 即可。

漫长的等待,结束了就可以使用这个优盘来安装系统了。

修改BIOS引导

msruefi,只能选择后者。一般新买的电脑或者近几年的电脑都是这个。 简单的识别方法:开机的时候,如果是品牌 Logo 然后就开始转圈圈进入 Windows 系统的话,就是 uefi。 第二就是修改 biossecurity boot,直接关闭就完事了。如果同等级的还有说什么引导什么系统的话,记得把它修改成 other

如果需要双系统

如果不需要双系统安装,直接跳过这一段

双系统指的是 Windows + osx,好像一般初学者都需要这个东西(包括之前的我自己)? 首先首先,Windows 默认的引导分区太小了,如果你不重新安装 Windows 的话,直接安装 osx 后面都会导致任何软件都无法安装。 教程 类似于上面地址里面的教程,但是在创建 efi 的时候,大小给大点,我一般给的是 512m,因为后面我们可能还需要根据不同的机型,在这个分区里面放入驱动。 分好区,先把 Windows 装上,记得留一些硬盘给 osx,我建议是 100G

装好系统后,在硬盘管理那里给 efi 分区随便给个盘符,然后把里面的东西备份一份出来,因为待会需要对他开刀,备份完整不至于你总是需要重新安装系统。

优盘引导开机

为了安装方便我建议将 first boot 设置为上面的优盘。当然使用 F12 来选择也行不过中途需要重启的时候就要留意重新选择了。 选择优盘中的带有 UEFI 选项的,这时候电脑将会读取优盘的引导,进入引导界面。

进入优盘是这个样子的:

一般是在第一项,下面会提示 Boot macOS Install from xxx 后面一般是优盘它默认给的名字。此时,我们 需要按一次 空格键 ,来到下一个页面:

下面是启动选项,一般按下空格需要设定的,大佬已经帮我们写好了,这个时候直接 按下回车键 进入安装软件就可以了。 一串命令后,我们会进来到这个页面:

能够来到这里,说明安装过程十分顺利,接下来进入下一个环节。

硬盘分区

就跟上面一样,我们选择磁盘工具,然后点击 继续,就会进入到磁盘工具的页面,那我们先选择显示所有设备,这时候磁盘都会显示出来。

然后我们选择整个硬盘(如果是单系统的话,双系统的话,你需要选择你要格式化的那个分区)点击上面的抹掉(看我鼠标那个位置):

点击之后会出来这个硬盘设定界面:

  1. 名称,可以自定义;

  2. 格式,新出来的文件系统是 APFS 听说是提速以及增加安全性的,那我就用这个好了,也建议使用这个。强烈不要使用区分大小写的分区格式(敲黑板),因为我使用了大小写区分的硬盘导致有些软件它不支持!!我目前就是因为这个原因重新安装系统的;

  3. 方案选择 GUID 分区图

稍稍等待一番,成功以后,直接左上角关闭磁盘管理工具。我们就可以安装系统了。

安装系统

回到这个界面,选择 安装 macOS


然后就同意协议了,这个不用图都知道要选择什么吧… 同意协议之后就是选择硬盘了,在这个界面,没有意外的话应该可以看到我们刚刚分出来的硬盘分区:

选好硬盘点击安装,接下来进入漫长的等待:

!!!请注意!!!几分钟动一次鼠标,不要让电脑进入睡眠,因为可能因为驱动原因会睡死。 然后系统在安装过程需要一次重启,别担心,如果你在之前已经设置好优盘第一启动了的话,就不用管它,没有的话,需要手动选择优盘引导,进入优盘不用做什么,会继续安装:

跑完基本就可以进入系统的基础设置页面了:

选择什么不用说了吧,选择其他国家也可以,只要看得懂就好…

完成安装

完成安装可以进入桌面了,左上角有个 -> 关于本机

哎哟卧槽,这个显存不对劲啊….. 那现在我们暂时忍一下,使用憋屈的动画效果使用一下 Safari,去下载一个软件:Clover Configurator 安装软件

LaunchPad 打开运行,不出意外的话:

LaunchPad/其他/终端 运行以下命令,输入设置的用户密码就可以了:

1
sudo spctl --master-disable

重新打开 Clover Configurator 来到这个菜单:

我们现在的引导还是优盘,所以先在优盘那一项点击后面的挂载,这时候应该需要输入用户的密码,挂载成功完以后,就可以在 访达 看到 EFI 分区了。

还记得这张图吗:

将原来的

config.plist 备份一份,把这里面的适合自己机型的配置,改名为 config.plist 放在上一层文件夹:

然后重启电脑,进入优盘引导,根据下面字样提示,选择 Boot macOS from 你设置的分区名 即可进入安装好的系统。如果配置兼容的话,重新查看关于本机:

显存正常了。 再尝试音乐,如果声卡也正常,那就完成安装了。

迁入硬盘引导

一切好了,总不能总是插着硬盘来开机吧,我们需要把硬盘的文件替换到磁盘里面去,还是使用 Clover Configurator

一个是硬盘的,一个是优盘的,同时挂载出来。 然后使用 访达 进行操作:

OK,bios 重新设置第一启动是硬盘,然后,如果需要进入 osx 则选择 osx 硬盘回车,需要 Windows 的话就选择 Windows 所在的分区即可。

其他设置

这是我自己觉得比较好的设置,每个人可以按照自己的习惯来。 当然关于原来在 Windows 习惯的 Ctrl + c 这些都需要换成 Command + C 了,Command 在原来的 Win键 上面,我是不建议在系统设置里面替换这两者的按键位置的,毕竟用起来还蛮舒服的。 然后所有软件需要进入设置界面,都已经统一是 Command + , 这个快捷键了。

访达小设置

访达默认设置我感觉是不太好用的,比如点击访达,显示的不是我的电脑的内容,而是最近使用的内容,我特么写代码最近使用的内容都是代码文件,而我们通常不需要在这里去访问这些文件的(都是通过 IDE 工具),所以我希望点击访达进来的时候就是 我的个人文件夹: 打开 访达Command + ,

第二个不好的点是,我去到那个文件夹,要搜索某个文件,默认给我跑整个电脑去搜索,速度慢又没必要,所以:

第三个不好的点是,文件默认居然不排序,还要像垃圾堆一样堆在一起!!改掉设置!: 这个不在 Command + , 里边了,在这里:

开启Retina渲染

参考资料: 黑苹果开启缩放分辨率HiDPi以及字体模糊的调整方法总结 黑苹果开启HiDPI“解决”字体模糊的问题 直接在 其他软件/终端 粘贴下面的命令运行,提示输入用户密码,输入完以后根据提示来走就可以了:

1
sh -c "$(curl -fsSL https://raw.githubusercontent.com/xzhih/one-key-hidpi/master/hidpi.sh)"

重启电脑后安装 RDM 工具来配置分辨率(下载 RDM-2.2.dmg 就可以了 ) 运行,右上角:

选择带有小闪电的就是 HiDPI 的设置(也就是 Retina 效果)

一般根据屏幕原始分辨率来定,有 4K 显示器开 2K 分辨率的话,效果就跟白苹果一样了,2倍的差距,我目前原始分辨率是 1920 * 1080,开图中的分辨率也就凑合凑合过吧

然后在 系统偏好设置 –> 通用 里面开启字体平滑(在最下面,默认也是开启的)

我对默认的平滑程度还是不太喜欢,要开最高的,终端运行以下命令,提示输入用户密码,输入完以后重启即可:

1
defaults -currentHost write -globalDomain AppleFontSmoothing -int 3

来看看效果图: 开启前: 开启后:

结束

后面有空再更更常用软件了。
然后我们选择整个硬盘(如果是单系统的话,双系统的话,你需要选择你要格式化的那个分区)点击上面的抹掉(看我鼠标那个位置):

从大学就开始折腾黑苹果,迟迟没有做些记录,总觉得差点什么,所以这次趁我需要重装系统的机会,拍了些照片,结合一些文字来做一个教程吧。 阅读按照文章顺序来,不要跳着来,除非我注明让你可以跳过。

OSX系统

不做评价谁好谁差,但是作为 Web工程师 的话,在 osx 系统下的编程体验确实要比 Windows 下好很多,软件也特别干净,从来没有在你工作的时候,突然右下角来个弹窗给你看广告,或者直接把广告就弹在最顶层来影响你的工作。 jdk npm git Dart Flutter 这些程序,在 unix 系的 osx 下也从来没有报错。所以还是推荐使用的。

安装须知

台式机

在我看来台式机安装黑果是比较完美的,能够屏蔽独立显卡,没有 WiFi 的顾虑。而且较高的性能使得能够更加完美的体验 osx 系统。 但是还是有些限制你需要知道:

  1. Intel 芯支持比较完美,如果是 AMD 芯的话,虽然说现在有破解,但是我还是觉得不如自然免驱的好(无测试过,没什么发言权);
  2. 一般比较热门的独立显卡,在国内远景论坛都有驱动的教程,涉及一些代码的注入,我比较懒,所以公司的电脑就拆掉了独显,使用集显来驱动,集显也是免驱的;

笔记本

  1. 笔记本受限比较大,现在笔记本一般都是 集显+独显 来自动切换达到省电的功能的,但是白苹果的笔记本,不是一个独显就是一个集显。所以,两者混合的情况下一般只能驱动集显(除非部分笔记本可以屏蔽集显),显卡问题就是独显基本就废了;
  2. WiFi 常年无解,两个方法解决:
    1. 淘宝购买 usb 接口的免驱网卡,卖家通常会提供使用所需要的驱动和软件;
    2. 更换笔记本内部的网卡,这一步比较专业,加上联想笔记本更换配件是需要刷白名单的,所以通常来说,都会选择第一种方式解决。

安装流程

镜像下载

因为在这篇文章产生的时候最新的 osx 系统是 10.15 catalina。所以我就是用这个系统来做教程吧。 osx_10.15_catalina 下载地址:下载地址

感谢远景论坛大佬的封装,我没有直接放大佬的最终下载地址,因为我还是希望各位能够给去下载,大佬有硬币赚。

镜像已经下好了,大佬也提供了常用的机型配置:

这一步只是先让你们看,先放一边,我们通常安装的时候不会直接选择合适的驱动,而是先使用通用驱动来安装,他是以最小的通用兼容配置来做的,通常都能够进入安装并且顺利通过。

写入优盘

此时,你需要拥有一个 16G 的优盘!记住记住,一定是 16G 不然他镜像可以刻录但是不会帮你刻录完整,不完整的镜像就是一个无法安装的镜像。(我用了一个 8G 的在这一步卡了好几个小时,最后把公司的师弟杀了祭天了,他出现了警告也不看直接点击 OK 就给我刻录了一个废盘) 首先你需要一个软件:TransMac 当然因为我的习惯,所以我都是在 Windows 系统下写优盘的。 安装,然后右键 -> 以管理员身份运行

会有提示需要购买正版什么的,不过点击 run 可以直接运行试用版,试用版就够用了。 然后找到你的优盘,右键选择 Format Disk –> Format with Disk Image 即可。

漫长的等待,结束了就可以使用这个优盘来安装系统了。

修改BIOS引导

msruefi,只能选择后者。一般新买的电脑或者近几年的电脑都是这个。 简单的识别方法:开机的时候,如果是品牌 Logo 然后就开始转圈圈进入 Windows 系统的话,就是 uefi。 第二就是修改 biossecurity boot,直接关闭就完事了。如果同等级的还有说什么引导什么系统的话,记得把它修改成 other

如果需要双系统

如果不需要双系统安装,直接跳过这一段

双系统指的是 Windows + osx,好像一般初学者都需要这个东西(包括之前的我自己)? 首先首先,Windows 默认的引导分区太小了,如果你不重新安装 Windows 的话,直接安装 osx 后面都会导致任何软件都无法安装。 教程 类似于上面地址里面的教程,但是在创建 efi 的时候,大小给大点,我一般给的是 512m,因为后面我们可能还需要根据不同的机型,在这个分区里面放入驱动。 分好区,先把 Windows 装上,记得留一些硬盘给 osx,我建议是 100G

装好系统后,在硬盘管理那里给 efi 分区随便给个盘符,然后把里面的东西备份一份出来,因为待会需要对他开刀,备份完整不至于你总是需要重新安装系统。

优盘引导开机

为了安装方便我建议将 first boot 设置为上面的优盘。当然使用 F12 来选择也行不过中途需要重启的时候就要留意重新选择了。 选择优盘中的带有 UEFI 选项的,这时候电脑将会读取优盘的引导,进入引导界面。

进入优盘是这个样子的:

一般是在第一项,下面会提示 Boot macOS Install from xxx 后面一般是优盘它默认给的名字。此时,我们 需要按一次 空格键 ,来到下一个页面:

下面是启动选项,一般按下空格需要设定的,大佬已经帮我们写好了,这个时候直接 按下回车键 进入安装软件就可以了。 一串命令后,我们会进来到这个页面:

能够来到这里,说明安装过程十分顺利,接下来进入下一个环节。

硬盘分区

就跟上面一样,我们选择磁盘工具,然后点击 继续,就会进入到磁盘工具的页面,那我们先选择显示所有设备,这时候磁盘都会显示出来。

然后我们选择整个硬盘(如果是单系统的话,双系统的话,你需要选择你要格式化的那个分区)点击上面的抹掉(看我鼠标那个位置):

点击之后会出来这个硬盘设定界面:

  1. 名称,可以自定义;

  2. 格式,新出来的文件系统是 APFS 听说是提速以及增加安全性的,那我就用这个好了,也建议使用这个。强烈不要使用区分大小写的分区格式(敲黑板),因为我使用了大小写区分的硬盘导致有些软件它不支持!!我目前就是因为这个原因重新安装系统的;

  3. 方案选择 GUID 分区图

稍稍等待一番,成功以后,直接左上角关闭磁盘管理工具。我们就可以安装系统了。

安装系统

回到这个界面,选择 安装 macOS


然后就同意协议了,这个不用图都知道要选择什么吧… 同意协议之后就是选择硬盘了,在这个界面,没有意外的话应该可以看到我们刚刚分出来的硬盘分区:

选好硬盘点击安装,接下来进入漫长的等待:

!!!请注意!!!几分钟动一次鼠标,不要让电脑进入睡眠,因为可能因为驱动原因会睡死。 然后系统在安装过程需要一次重启,别担心,如果你在之前已经设置好优盘第一启动了的话,就不用管它,没有的话,需要手动选择优盘引导,进入优盘不用做什么,会继续安装:

跑完基本就可以进入系统的基础设置页面了:

选择什么不用说了吧,选择其他国家也可以,只要看得懂就好…

完成安装

完成安装可以进入桌面了,左上角有个 -> 关于本机

哎哟卧槽,这个显存不对劲啊….. 那现在我们暂时忍一下,使用憋屈的动画效果使用一下 Safari,去下载一个软件:Clover Configurator 安装软件

LaunchPad 打开运行,不出意外的话:

LaunchPad/其他/终端 运行以下命令,输入设置的用户密码就可以了:

1
sudo spctl --master-disable

重新打开 Clover Configurator 来到这个菜单:

我们现在的引导还是优盘,所以先在优盘那一项点击后面的挂载,这时候应该需要输入用户的密码,挂载成功完以后,就可以在 访达 看到 EFI 分区了。

还记得这张图吗:

将原来的

config.plist 备份一份,把这里面的适合自己机型的配置,改名为 config.plist 放在上一层文件夹:

然后重启电脑,进入优盘引导,根据下面字样提示,选择 Boot macOS from 你设置的分区名 即可进入安装好的系统。如果配置兼容的话,重新查看关于本机:

显存正常了。 再尝试音乐,如果声卡也正常,那就完成安装了。

迁入硬盘引导

一切好了,总不能总是插着硬盘来开机吧,我们需要把硬盘的文件替换到磁盘里面去,还是使用 Clover Configurator

一个是硬盘的,一个是优盘的,同时挂载出来。 然后使用 访达 进行操作:

OK,bios 重新设置第一启动是硬盘,然后,如果需要进入 osx 则选择 osx 硬盘回车,需要 Windows 的话就选择 Windows 所在的分区即可。

其他设置

这是我自己觉得比较好的设置,每个人可以按照自己的习惯来。 当然关于原来在 Windows 习惯的 Ctrl + c 这些都需要换成 Command + C 了,Command 在原来的 Win键 上面,我是不建议在系统设置里面替换这两者的按键位置的,毕竟用起来还蛮舒服的。 然后所有软件需要进入设置界面,都已经统一是 Command + , 这个快捷键了。

访达小设置

访达默认设置我感觉是不太好用的,比如点击访达,显示的不是我的电脑的内容,而是最近使用的内容,我特么写代码最近使用的内容都是代码文件,而我们通常不需要在这里去访问这些文件的(都是通过 IDE 工具),所以我希望点击访达进来的时候就是 我的个人文件夹: 打开 访达Command + ,

第二个不好的点是,我去到那个文件夹,要搜索某个文件,默认给我跑整个电脑去搜索,速度慢又没必要,所以:

第三个不好的点是,文件默认居然不排序,还要像垃圾堆一样堆在一起!!改掉设置!: 这个不在 Command + , 里边了,在这里:

开启Retina渲染

参考资料: 黑苹果开启缩放分辨率HiDPi以及字体模糊的调整方法总结 黑苹果开启HiDPI“解决”字体模糊的问题 直接在 其他软件/终端 粘贴下面的命令运行,提示输入用户密码,输入完以后根据提示来走就可以了:

1
sh -c "$(curl -fsSL https://raw.githubusercontent.com/xzhih/one-key-hidpi/master/hidpi.sh)"

重启电脑后安装 RDM 工具来配置分辨率(下载 RDM-2.2.dmg 就可以了 ) 运行,右上角:

选择带有小闪电的就是 HiDPI 的设置(也就是 Retina 效果)

一般根据屏幕原始分辨率来定,有 4K 显示器开 2K 分辨率的话,效果就跟白苹果一样了,2倍的差距,我目前原始分辨率是 1920 * 1080,开图中的分辨率也就凑合凑合过吧

然后在 系统偏好设置 –> 通用 里面开启字体平滑(在最下面,默认也是开启的)

我对默认的平滑程度还是不太喜欢,要开最高的,终端运行以下命令,提示输入用户密码,输入完以后重启即可:

1
defaults -currentHost write -globalDomain AppleFontSmoothing -int 3

来看看效果图: 开启前: 开启后:

结束

后面有空再更更常用软件了。

点击之后会出来这个硬盘设定界面:

从大学就开始折腾黑苹果,迟迟没有做些记录,总觉得差点什么,所以这次趁我需要重装系统的机会,拍了些照片,结合一些文字来做一个教程吧。 阅读按照文章顺序来,不要跳着来,除非我注明让你可以跳过。

OSX系统

不做评价谁好谁差,但是作为 Web工程师 的话,在 osx 系统下的编程体验确实要比 Windows 下好很多,软件也特别干净,从来没有在你工作的时候,突然右下角来个弹窗给你看广告,或者直接把广告就弹在最顶层来影响你的工作。 jdk npm git Dart Flutter 这些程序,在 unix 系的 osx 下也从来没有报错。所以还是推荐使用的。

安装须知

台式机

在我看来台式机安装黑果是比较完美的,能够屏蔽独立显卡,没有 WiFi 的顾虑。而且较高的性能使得能够更加完美的体验 osx 系统。 但是还是有些限制你需要知道:

  1. Intel 芯支持比较完美,如果是 AMD 芯的话,虽然说现在有破解,但是我还是觉得不如自然免驱的好(无测试过,没什么发言权);
  2. 一般比较热门的独立显卡,在国内远景论坛都有驱动的教程,涉及一些代码的注入,我比较懒,所以公司的电脑就拆掉了独显,使用集显来驱动,集显也是免驱的;

笔记本

  1. 笔记本受限比较大,现在笔记本一般都是 集显+独显 来自动切换达到省电的功能的,但是白苹果的笔记本,不是一个独显就是一个集显。所以,两者混合的情况下一般只能驱动集显(除非部分笔记本可以屏蔽集显),显卡问题就是独显基本就废了;
  2. WiFi 常年无解,两个方法解决:
    1. 淘宝购买 usb 接口的免驱网卡,卖家通常会提供使用所需要的驱动和软件;
    2. 更换笔记本内部的网卡,这一步比较专业,加上联想笔记本更换配件是需要刷白名单的,所以通常来说,都会选择第一种方式解决。

安装流程

镜像下载

因为在这篇文章产生的时候最新的 osx 系统是 10.15 catalina。所以我就是用这个系统来做教程吧。 osx_10.15_catalina 下载地址:下载地址

感谢远景论坛大佬的封装,我没有直接放大佬的最终下载地址,因为我还是希望各位能够给去下载,大佬有硬币赚。

镜像已经下好了,大佬也提供了常用的机型配置:

这一步只是先让你们看,先放一边,我们通常安装的时候不会直接选择合适的驱动,而是先使用通用驱动来安装,他是以最小的通用兼容配置来做的,通常都能够进入安装并且顺利通过。

写入优盘

此时,你需要拥有一个 16G 的优盘!记住记住,一定是 16G 不然他镜像可以刻录但是不会帮你刻录完整,不完整的镜像就是一个无法安装的镜像。(我用了一个 8G 的在这一步卡了好几个小时,最后把公司的师弟杀了祭天了,他出现了警告也不看直接点击 OK 就给我刻录了一个废盘) 首先你需要一个软件:TransMac 当然因为我的习惯,所以我都是在 Windows 系统下写优盘的。 安装,然后右键 -> 以管理员身份运行

会有提示需要购买正版什么的,不过点击 run 可以直接运行试用版,试用版就够用了。 然后找到你的优盘,右键选择 Format Disk –> Format with Disk Image 即可。

漫长的等待,结束了就可以使用这个优盘来安装系统了。

修改BIOS引导

msruefi,只能选择后者。一般新买的电脑或者近几年的电脑都是这个。 简单的识别方法:开机的时候,如果是品牌 Logo 然后就开始转圈圈进入 Windows 系统的话,就是 uefi。 第二就是修改 biossecurity boot,直接关闭就完事了。如果同等级的还有说什么引导什么系统的话,记得把它修改成 other

如果需要双系统

如果不需要双系统安装,直接跳过这一段

双系统指的是 Windows + osx,好像一般初学者都需要这个东西(包括之前的我自己)? 首先首先,Windows 默认的引导分区太小了,如果你不重新安装 Windows 的话,直接安装 osx 后面都会导致任何软件都无法安装。 教程 类似于上面地址里面的教程,但是在创建 efi 的时候,大小给大点,我一般给的是 512m,因为后面我们可能还需要根据不同的机型,在这个分区里面放入驱动。 分好区,先把 Windows 装上,记得留一些硬盘给 osx,我建议是 100G

装好系统后,在硬盘管理那里给 efi 分区随便给个盘符,然后把里面的东西备份一份出来,因为待会需要对他开刀,备份完整不至于你总是需要重新安装系统。

优盘引导开机

为了安装方便我建议将 first boot 设置为上面的优盘。当然使用 F12 来选择也行不过中途需要重启的时候就要留意重新选择了。 选择优盘中的带有 UEFI 选项的,这时候电脑将会读取优盘的引导,进入引导界面。

进入优盘是这个样子的:

一般是在第一项,下面会提示 Boot macOS Install from xxx 后面一般是优盘它默认给的名字。此时,我们 需要按一次 空格键 ,来到下一个页面:

下面是启动选项,一般按下空格需要设定的,大佬已经帮我们写好了,这个时候直接 按下回车键 进入安装软件就可以了。 一串命令后,我们会进来到这个页面:

能够来到这里,说明安装过程十分顺利,接下来进入下一个环节。

硬盘分区

就跟上面一样,我们选择磁盘工具,然后点击 继续,就会进入到磁盘工具的页面,那我们先选择显示所有设备,这时候磁盘都会显示出来。

然后我们选择整个硬盘(如果是单系统的话,双系统的话,你需要选择你要格式化的那个分区)点击上面的抹掉(看我鼠标那个位置):

点击之后会出来这个硬盘设定界面:

  1. 名称,可以自定义;

  2. 格式,新出来的文件系统是 APFS 听说是提速以及增加安全性的,那我就用这个好了,也建议使用这个。强烈不要使用区分大小写的分区格式(敲黑板),因为我使用了大小写区分的硬盘导致有些软件它不支持!!我目前就是因为这个原因重新安装系统的;

  3. 方案选择 GUID 分区图

稍稍等待一番,成功以后,直接左上角关闭磁盘管理工具。我们就可以安装系统了。

安装系统

回到这个界面,选择 安装 macOS


然后就同意协议了,这个不用图都知道要选择什么吧… 同意协议之后就是选择硬盘了,在这个界面,没有意外的话应该可以看到我们刚刚分出来的硬盘分区:

选好硬盘点击安装,接下来进入漫长的等待:

!!!请注意!!!几分钟动一次鼠标,不要让电脑进入睡眠,因为可能因为驱动原因会睡死。 然后系统在安装过程需要一次重启,别担心,如果你在之前已经设置好优盘第一启动了的话,就不用管它,没有的话,需要手动选择优盘引导,进入优盘不用做什么,会继续安装:

跑完基本就可以进入系统的基础设置页面了:

选择什么不用说了吧,选择其他国家也可以,只要看得懂就好…

完成安装

完成安装可以进入桌面了,左上角有个 -> 关于本机

哎哟卧槽,这个显存不对劲啊….. 那现在我们暂时忍一下,使用憋屈的动画效果使用一下 Safari,去下载一个软件:Clover Configurator 安装软件

LaunchPad 打开运行,不出意外的话:

LaunchPad/其他/终端 运行以下命令,输入设置的用户密码就可以了:

1
sudo spctl --master-disable

重新打开 Clover Configurator 来到这个菜单:

我们现在的引导还是优盘,所以先在优盘那一项点击后面的挂载,这时候应该需要输入用户的密码,挂载成功完以后,就可以在 访达 看到 EFI 分区了。

还记得这张图吗:

将原来的

config.plist 备份一份,把这里面的适合自己机型的配置,改名为 config.plist 放在上一层文件夹:

然后重启电脑,进入优盘引导,根据下面字样提示,选择 Boot macOS from 你设置的分区名 即可进入安装好的系统。如果配置兼容的话,重新查看关于本机:

显存正常了。 再尝试音乐,如果声卡也正常,那就完成安装了。

迁入硬盘引导

一切好了,总不能总是插着硬盘来开机吧,我们需要把硬盘的文件替换到磁盘里面去,还是使用 Clover Configurator

一个是硬盘的,一个是优盘的,同时挂载出来。 然后使用 访达 进行操作:

OK,bios 重新设置第一启动是硬盘,然后,如果需要进入 osx 则选择 osx 硬盘回车,需要 Windows 的话就选择 Windows 所在的分区即可。

其他设置

这是我自己觉得比较好的设置,每个人可以按照自己的习惯来。 当然关于原来在 Windows 习惯的 Ctrl + c 这些都需要换成 Command + C 了,Command 在原来的 Win键 上面,我是不建议在系统设置里面替换这两者的按键位置的,毕竟用起来还蛮舒服的。 然后所有软件需要进入设置界面,都已经统一是 Command + , 这个快捷键了。

访达小设置

访达默认设置我感觉是不太好用的,比如点击访达,显示的不是我的电脑的内容,而是最近使用的内容,我特么写代码最近使用的内容都是代码文件,而我们通常不需要在这里去访问这些文件的(都是通过 IDE 工具),所以我希望点击访达进来的时候就是 我的个人文件夹: 打开 访达Command + ,

第二个不好的点是,我去到那个文件夹,要搜索某个文件,默认给我跑整个电脑去搜索,速度慢又没必要,所以:

第三个不好的点是,文件默认居然不排序,还要像垃圾堆一样堆在一起!!改掉设置!: 这个不在 Command + , 里边了,在这里:

开启Retina渲染

参考资料: 黑苹果开启缩放分辨率HiDPi以及字体模糊的调整方法总结 黑苹果开启HiDPI“解决”字体模糊的问题 直接在 其他软件/终端 粘贴下面的命令运行,提示输入用户密码,输入完以后根据提示来走就可以了:

1
sh -c "$(curl -fsSL https://raw.githubusercontent.com/xzhih/one-key-hidpi/master/hidpi.sh)"

重启电脑后安装 RDM 工具来配置分辨率(下载 RDM-2.2.dmg 就可以了 ) 运行,右上角:

选择带有小闪电的就是 HiDPI 的设置(也就是 Retina 效果)

一般根据屏幕原始分辨率来定,有 4K 显示器开 2K 分辨率的话,效果就跟白苹果一样了,2倍的差距,我目前原始分辨率是 1920 * 1080,开图中的分辨率也就凑合凑合过吧

然后在 系统偏好设置 –> 通用 里面开启字体平滑(在最下面,默认也是开启的)

我对默认的平滑程度还是不太喜欢,要开最高的,终端运行以下命令,提示输入用户密码,输入完以后重启即可:

1
defaults -currentHost write -globalDomain AppleFontSmoothing -int 3

来看看效果图: 开启前: 开启后:

结束

后面有空再更更常用软件了。

  1. 名称,可以自定义;

  2. 格式,新出来的文件系统是 APFS 听说是提速以及增加安全性的,那我就用这个好了,也建议使用这个。强烈不要使用区分大小写的分区格式(敲黑板),因为我使用了大小写区分的硬盘导致有些软件它不支持!!我目前就是因为这个原因重新安装系统的;

  3. 方案选择 GUID 分区图

稍稍等待一番,成功以后,直接左上角关闭磁盘管理工具。我们就可以安装系统了。

安装系统

回到这个界面,选择 安装 macOS

从大学就开始折腾黑苹果,迟迟没有做些记录,总觉得差点什么,所以这次趁我需要重装系统的机会,拍了些照片,结合一些文字来做一个教程吧。 阅读按照文章顺序来,不要跳着来,除非我注明让你可以跳过。

OSX系统

不做评价谁好谁差,但是作为 Web工程师 的话,在 osx 系统下的编程体验确实要比 Windows 下好很多,软件也特别干净,从来没有在你工作的时候,突然右下角来个弹窗给你看广告,或者直接把广告就弹在最顶层来影响你的工作。 jdk npm git Dart Flutter 这些程序,在 unix 系的 osx 下也从来没有报错。所以还是推荐使用的。

安装须知

台式机

在我看来台式机安装黑果是比较完美的,能够屏蔽独立显卡,没有 WiFi 的顾虑。而且较高的性能使得能够更加完美的体验 osx 系统。 但是还是有些限制你需要知道:

  1. Intel 芯支持比较完美,如果是 AMD 芯的话,虽然说现在有破解,但是我还是觉得不如自然免驱的好(无测试过,没什么发言权);
  2. 一般比较热门的独立显卡,在国内远景论坛都有驱动的教程,涉及一些代码的注入,我比较懒,所以公司的电脑就拆掉了独显,使用集显来驱动,集显也是免驱的;

笔记本

  1. 笔记本受限比较大,现在笔记本一般都是 集显+独显 来自动切换达到省电的功能的,但是白苹果的笔记本,不是一个独显就是一个集显。所以,两者混合的情况下一般只能驱动集显(除非部分笔记本可以屏蔽集显),显卡问题就是独显基本就废了;
  2. WiFi 常年无解,两个方法解决:
    1. 淘宝购买 usb 接口的免驱网卡,卖家通常会提供使用所需要的驱动和软件;
    2. 更换笔记本内部的网卡,这一步比较专业,加上联想笔记本更换配件是需要刷白名单的,所以通常来说,都会选择第一种方式解决。

安装流程

镜像下载

因为在这篇文章产生的时候最新的 osx 系统是 10.15 catalina。所以我就是用这个系统来做教程吧。 osx_10.15_catalina 下载地址:下载地址

感谢远景论坛大佬的封装,我没有直接放大佬的最终下载地址,因为我还是希望各位能够给去下载,大佬有硬币赚。

镜像已经下好了,大佬也提供了常用的机型配置:

这一步只是先让你们看,先放一边,我们通常安装的时候不会直接选择合适的驱动,而是先使用通用驱动来安装,他是以最小的通用兼容配置来做的,通常都能够进入安装并且顺利通过。

写入优盘

此时,你需要拥有一个 16G 的优盘!记住记住,一定是 16G 不然他镜像可以刻录但是不会帮你刻录完整,不完整的镜像就是一个无法安装的镜像。(我用了一个 8G 的在这一步卡了好几个小时,最后把公司的师弟杀了祭天了,他出现了警告也不看直接点击 OK 就给我刻录了一个废盘) 首先你需要一个软件:TransMac 当然因为我的习惯,所以我都是在 Windows 系统下写优盘的。 安装,然后右键 -> 以管理员身份运行

会有提示需要购买正版什么的,不过点击 run 可以直接运行试用版,试用版就够用了。 然后找到你的优盘,右键选择 Format Disk –> Format with Disk Image 即可。

漫长的等待,结束了就可以使用这个优盘来安装系统了。

修改BIOS引导

msruefi,只能选择后者。一般新买的电脑或者近几年的电脑都是这个。 简单的识别方法:开机的时候,如果是品牌 Logo 然后就开始转圈圈进入 Windows 系统的话,就是 uefi。 第二就是修改 biossecurity boot,直接关闭就完事了。如果同等级的还有说什么引导什么系统的话,记得把它修改成 other

如果需要双系统

如果不需要双系统安装,直接跳过这一段

双系统指的是 Windows + osx,好像一般初学者都需要这个东西(包括之前的我自己)? 首先首先,Windows 默认的引导分区太小了,如果你不重新安装 Windows 的话,直接安装 osx 后面都会导致任何软件都无法安装。 教程 类似于上面地址里面的教程,但是在创建 efi 的时候,大小给大点,我一般给的是 512m,因为后面我们可能还需要根据不同的机型,在这个分区里面放入驱动。 分好区,先把 Windows 装上,记得留一些硬盘给 osx,我建议是 100G

装好系统后,在硬盘管理那里给 efi 分区随便给个盘符,然后把里面的东西备份一份出来,因为待会需要对他开刀,备份完整不至于你总是需要重新安装系统。

优盘引导开机

为了安装方便我建议将 first boot 设置为上面的优盘。当然使用 F12 来选择也行不过中途需要重启的时候就要留意重新选择了。 选择优盘中的带有 UEFI 选项的,这时候电脑将会读取优盘的引导,进入引导界面。

进入优盘是这个样子的:

一般是在第一项,下面会提示 Boot macOS Install from xxx 后面一般是优盘它默认给的名字。此时,我们 需要按一次 空格键 ,来到下一个页面:

下面是启动选项,一般按下空格需要设定的,大佬已经帮我们写好了,这个时候直接 按下回车键 进入安装软件就可以了。 一串命令后,我们会进来到这个页面:

能够来到这里,说明安装过程十分顺利,接下来进入下一个环节。

硬盘分区

就跟上面一样,我们选择磁盘工具,然后点击 继续,就会进入到磁盘工具的页面,那我们先选择显示所有设备,这时候磁盘都会显示出来。

然后我们选择整个硬盘(如果是单系统的话,双系统的话,你需要选择你要格式化的那个分区)点击上面的抹掉(看我鼠标那个位置):

点击之后会出来这个硬盘设定界面:

  1. 名称,可以自定义;

  2. 格式,新出来的文件系统是 APFS 听说是提速以及增加安全性的,那我就用这个好了,也建议使用这个。强烈不要使用区分大小写的分区格式(敲黑板),因为我使用了大小写区分的硬盘导致有些软件它不支持!!我目前就是因为这个原因重新安装系统的;

  3. 方案选择 GUID 分区图

稍稍等待一番,成功以后,直接左上角关闭磁盘管理工具。我们就可以安装系统了。

安装系统

回到这个界面,选择 安装 macOS


然后就同意协议了,这个不用图都知道要选择什么吧… 同意协议之后就是选择硬盘了,在这个界面,没有意外的话应该可以看到我们刚刚分出来的硬盘分区:

选好硬盘点击安装,接下来进入漫长的等待:

!!!请注意!!!几分钟动一次鼠标,不要让电脑进入睡眠,因为可能因为驱动原因会睡死。 然后系统在安装过程需要一次重启,别担心,如果你在之前已经设置好优盘第一启动了的话,就不用管它,没有的话,需要手动选择优盘引导,进入优盘不用做什么,会继续安装:

跑完基本就可以进入系统的基础设置页面了:

选择什么不用说了吧,选择其他国家也可以,只要看得懂就好…

完成安装

完成安装可以进入桌面了,左上角有个 -> 关于本机

哎哟卧槽,这个显存不对劲啊….. 那现在我们暂时忍一下,使用憋屈的动画效果使用一下 Safari,去下载一个软件:Clover Configurator 安装软件

LaunchPad 打开运行,不出意外的话:

LaunchPad/其他/终端 运行以下命令,输入设置的用户密码就可以了:

1
sudo spctl --master-disable

重新打开 Clover Configurator 来到这个菜单:

我们现在的引导还是优盘,所以先在优盘那一项点击后面的挂载,这时候应该需要输入用户的密码,挂载成功完以后,就可以在 访达 看到 EFI 分区了。

还记得这张图吗:

将原来的

config.plist 备份一份,把这里面的适合自己机型的配置,改名为 config.plist 放在上一层文件夹:

然后重启电脑,进入优盘引导,根据下面字样提示,选择 Boot macOS from 你设置的分区名 即可进入安装好的系统。如果配置兼容的话,重新查看关于本机:

显存正常了。 再尝试音乐,如果声卡也正常,那就完成安装了。

迁入硬盘引导

一切好了,总不能总是插着硬盘来开机吧,我们需要把硬盘的文件替换到磁盘里面去,还是使用 Clover Configurator

一个是硬盘的,一个是优盘的,同时挂载出来。 然后使用 访达 进行操作:

OK,bios 重新设置第一启动是硬盘,然后,如果需要进入 osx 则选择 osx 硬盘回车,需要 Windows 的话就选择 Windows 所在的分区即可。

其他设置

这是我自己觉得比较好的设置,每个人可以按照自己的习惯来。 当然关于原来在 Windows 习惯的 Ctrl + c 这些都需要换成 Command + C 了,Command 在原来的 Win键 上面,我是不建议在系统设置里面替换这两者的按键位置的,毕竟用起来还蛮舒服的。 然后所有软件需要进入设置界面,都已经统一是 Command + , 这个快捷键了。

访达小设置

访达默认设置我感觉是不太好用的,比如点击访达,显示的不是我的电脑的内容,而是最近使用的内容,我特么写代码最近使用的内容都是代码文件,而我们通常不需要在这里去访问这些文件的(都是通过 IDE 工具),所以我希望点击访达进来的时候就是 我的个人文件夹: 打开 访达Command + ,

第二个不好的点是,我去到那个文件夹,要搜索某个文件,默认给我跑整个电脑去搜索,速度慢又没必要,所以:

第三个不好的点是,文件默认居然不排序,还要像垃圾堆一样堆在一起!!改掉设置!: 这个不在 Command + , 里边了,在这里:

开启Retina渲染

参考资料: 黑苹果开启缩放分辨率HiDPi以及字体模糊的调整方法总结 黑苹果开启HiDPI“解决”字体模糊的问题 直接在 其他软件/终端 粘贴下面的命令运行,提示输入用户密码,输入完以后根据提示来走就可以了:

1
sh -c "$(curl -fsSL https://raw.githubusercontent.com/xzhih/one-key-hidpi/master/hidpi.sh)"

重启电脑后安装 RDM 工具来配置分辨率(下载 RDM-2.2.dmg 就可以了 ) 运行,右上角:

选择带有小闪电的就是 HiDPI 的设置(也就是 Retina 效果)

一般根据屏幕原始分辨率来定,有 4K 显示器开 2K 分辨率的话,效果就跟白苹果一样了,2倍的差距,我目前原始分辨率是 1920 * 1080,开图中的分辨率也就凑合凑合过吧

然后在 系统偏好设置 –> 通用 里面开启字体平滑(在最下面,默认也是开启的)

我对默认的平滑程度还是不太喜欢,要开最高的,终端运行以下命令,提示输入用户密码,输入完以后重启即可:

1
defaults -currentHost write -globalDomain AppleFontSmoothing -int 3

来看看效果图: 开启前: 开启后:

结束

后面有空再更更常用软件了。

从大学就开始折腾黑苹果,迟迟没有做些记录,总觉得差点什么,所以这次趁我需要重装系统的机会,拍了些照片,结合一些文字来做一个教程吧。 阅读按照文章顺序来,不要跳着来,除非我注明让你可以跳过。

OSX系统

不做评价谁好谁差,但是作为 Web工程师 的话,在 osx 系统下的编程体验确实要比 Windows 下好很多,软件也特别干净,从来没有在你工作的时候,突然右下角来个弹窗给你看广告,或者直接把广告就弹在最顶层来影响你的工作。 jdk npm git Dart Flutter 这些程序,在 unix 系的 osx 下也从来没有报错。所以还是推荐使用的。

安装须知

台式机

在我看来台式机安装黑果是比较完美的,能够屏蔽独立显卡,没有 WiFi 的顾虑。而且较高的性能使得能够更加完美的体验 osx 系统。 但是还是有些限制你需要知道:

  1. Intel 芯支持比较完美,如果是 AMD 芯的话,虽然说现在有破解,但是我还是觉得不如自然免驱的好(无测试过,没什么发言权);
  2. 一般比较热门的独立显卡,在国内远景论坛都有驱动的教程,涉及一些代码的注入,我比较懒,所以公司的电脑就拆掉了独显,使用集显来驱动,集显也是免驱的;

笔记本

  1. 笔记本受限比较大,现在笔记本一般都是 集显+独显 来自动切换达到省电的功能的,但是白苹果的笔记本,不是一个独显就是一个集显。所以,两者混合的情况下一般只能驱动集显(除非部分笔记本可以屏蔽集显),显卡问题就是独显基本就废了;
  2. WiFi 常年无解,两个方法解决:
    1. 淘宝购买 usb 接口的免驱网卡,卖家通常会提供使用所需要的驱动和软件;
    2. 更换笔记本内部的网卡,这一步比较专业,加上联想笔记本更换配件是需要刷白名单的,所以通常来说,都会选择第一种方式解决。

安装流程

镜像下载

因为在这篇文章产生的时候最新的 osx 系统是 10.15 catalina。所以我就是用这个系统来做教程吧。 osx_10.15_catalina 下载地址:下载地址

感谢远景论坛大佬的封装,我没有直接放大佬的最终下载地址,因为我还是希望各位能够给去下载,大佬有硬币赚。

镜像已经下好了,大佬也提供了常用的机型配置:

这一步只是先让你们看,先放一边,我们通常安装的时候不会直接选择合适的驱动,而是先使用通用驱动来安装,他是以最小的通用兼容配置来做的,通常都能够进入安装并且顺利通过。

写入优盘

此时,你需要拥有一个 16G 的优盘!记住记住,一定是 16G 不然他镜像可以刻录但是不会帮你刻录完整,不完整的镜像就是一个无法安装的镜像。(我用了一个 8G 的在这一步卡了好几个小时,最后把公司的师弟杀了祭天了,他出现了警告也不看直接点击 OK 就给我刻录了一个废盘) 首先你需要一个软件:TransMac 当然因为我的习惯,所以我都是在 Windows 系统下写优盘的。 安装,然后右键 -> 以管理员身份运行

会有提示需要购买正版什么的,不过点击 run 可以直接运行试用版,试用版就够用了。 然后找到你的优盘,右键选择 Format Disk –> Format with Disk Image 即可。

漫长的等待,结束了就可以使用这个优盘来安装系统了。

修改BIOS引导

msruefi,只能选择后者。一般新买的电脑或者近几年的电脑都是这个。 简单的识别方法:开机的时候,如果是品牌 Logo 然后就开始转圈圈进入 Windows 系统的话,就是 uefi。 第二就是修改 biossecurity boot,直接关闭就完事了。如果同等级的还有说什么引导什么系统的话,记得把它修改成 other

如果需要双系统

如果不需要双系统安装,直接跳过这一段

双系统指的是 Windows + osx,好像一般初学者都需要这个东西(包括之前的我自己)? 首先首先,Windows 默认的引导分区太小了,如果你不重新安装 Windows 的话,直接安装 osx 后面都会导致任何软件都无法安装。 教程 类似于上面地址里面的教程,但是在创建 efi 的时候,大小给大点,我一般给的是 512m,因为后面我们可能还需要根据不同的机型,在这个分区里面放入驱动。 分好区,先把 Windows 装上,记得留一些硬盘给 osx,我建议是 100G

装好系统后,在硬盘管理那里给 efi 分区随便给个盘符,然后把里面的东西备份一份出来,因为待会需要对他开刀,备份完整不至于你总是需要重新安装系统。

优盘引导开机

为了安装方便我建议将 first boot 设置为上面的优盘。当然使用 F12 来选择也行不过中途需要重启的时候就要留意重新选择了。 选择优盘中的带有 UEFI 选项的,这时候电脑将会读取优盘的引导,进入引导界面。

进入优盘是这个样子的:

一般是在第一项,下面会提示 Boot macOS Install from xxx 后面一般是优盘它默认给的名字。此时,我们 需要按一次 空格键 ,来到下一个页面:

下面是启动选项,一般按下空格需要设定的,大佬已经帮我们写好了,这个时候直接 按下回车键 进入安装软件就可以了。 一串命令后,我们会进来到这个页面:

能够来到这里,说明安装过程十分顺利,接下来进入下一个环节。

硬盘分区

就跟上面一样,我们选择磁盘工具,然后点击 继续,就会进入到磁盘工具的页面,那我们先选择显示所有设备,这时候磁盘都会显示出来。

然后我们选择整个硬盘(如果是单系统的话,双系统的话,你需要选择你要格式化的那个分区)点击上面的抹掉(看我鼠标那个位置):

点击之后会出来这个硬盘设定界面:

  1. 名称,可以自定义;

  2. 格式,新出来的文件系统是 APFS 听说是提速以及增加安全性的,那我就用这个好了,也建议使用这个。强烈不要使用区分大小写的分区格式(敲黑板),因为我使用了大小写区分的硬盘导致有些软件它不支持!!我目前就是因为这个原因重新安装系统的;

  3. 方案选择 GUID 分区图

稍稍等待一番,成功以后,直接左上角关闭磁盘管理工具。我们就可以安装系统了。

安装系统

回到这个界面,选择 安装 macOS


然后就同意协议了,这个不用图都知道要选择什么吧… 同意协议之后就是选择硬盘了,在这个界面,没有意外的话应该可以看到我们刚刚分出来的硬盘分区:

选好硬盘点击安装,接下来进入漫长的等待:

!!!请注意!!!几分钟动一次鼠标,不要让电脑进入睡眠,因为可能因为驱动原因会睡死。 然后系统在安装过程需要一次重启,别担心,如果你在之前已经设置好优盘第一启动了的话,就不用管它,没有的话,需要手动选择优盘引导,进入优盘不用做什么,会继续安装:

跑完基本就可以进入系统的基础设置页面了:

选择什么不用说了吧,选择其他国家也可以,只要看得懂就好…

完成安装

完成安装可以进入桌面了,左上角有个 -> 关于本机

哎哟卧槽,这个显存不对劲啊….. 那现在我们暂时忍一下,使用憋屈的动画效果使用一下 Safari,去下载一个软件:Clover Configurator 安装软件

LaunchPad 打开运行,不出意外的话:

LaunchPad/其他/终端 运行以下命令,输入设置的用户密码就可以了:

1
sudo spctl --master-disable

重新打开 Clover Configurator 来到这个菜单:

我们现在的引导还是优盘,所以先在优盘那一项点击后面的挂载,这时候应该需要输入用户的密码,挂载成功完以后,就可以在 访达 看到 EFI 分区了。

还记得这张图吗:

将原来的

config.plist 备份一份,把这里面的适合自己机型的配置,改名为 config.plist 放在上一层文件夹:

然后重启电脑,进入优盘引导,根据下面字样提示,选择 Boot macOS from 你设置的分区名 即可进入安装好的系统。如果配置兼容的话,重新查看关于本机:

显存正常了。 再尝试音乐,如果声卡也正常,那就完成安装了。

迁入硬盘引导

一切好了,总不能总是插着硬盘来开机吧,我们需要把硬盘的文件替换到磁盘里面去,还是使用 Clover Configurator

一个是硬盘的,一个是优盘的,同时挂载出来。 然后使用 访达 进行操作:

OK,bios 重新设置第一启动是硬盘,然后,如果需要进入 osx 则选择 osx 硬盘回车,需要 Windows 的话就选择 Windows 所在的分区即可。

其他设置

这是我自己觉得比较好的设置,每个人可以按照自己的习惯来。 当然关于原来在 Windows 习惯的 Ctrl + c 这些都需要换成 Command + C 了,Command 在原来的 Win键 上面,我是不建议在系统设置里面替换这两者的按键位置的,毕竟用起来还蛮舒服的。 然后所有软件需要进入设置界面,都已经统一是 Command + , 这个快捷键了。

访达小设置

访达默认设置我感觉是不太好用的,比如点击访达,显示的不是我的电脑的内容,而是最近使用的内容,我特么写代码最近使用的内容都是代码文件,而我们通常不需要在这里去访问这些文件的(都是通过 IDE 工具),所以我希望点击访达进来的时候就是 我的个人文件夹: 打开 访达Command + ,

第二个不好的点是,我去到那个文件夹,要搜索某个文件,默认给我跑整个电脑去搜索,速度慢又没必要,所以:

第三个不好的点是,文件默认居然不排序,还要像垃圾堆一样堆在一起!!改掉设置!: 这个不在 Command + , 里边了,在这里:

开启Retina渲染

参考资料: 黑苹果开启缩放分辨率HiDPi以及字体模糊的调整方法总结 黑苹果开启HiDPI“解决”字体模糊的问题 直接在 其他软件/终端 粘贴下面的命令运行,提示输入用户密码,输入完以后根据提示来走就可以了:

1
sh -c "$(curl -fsSL https://raw.githubusercontent.com/xzhih/one-key-hidpi/master/hidpi.sh)"

重启电脑后安装 RDM 工具来配置分辨率(下载 RDM-2.2.dmg 就可以了 ) 运行,右上角:

选择带有小闪电的就是 HiDPI 的设置(也就是 Retina 效果)

一般根据屏幕原始分辨率来定,有 4K 显示器开 2K 分辨率的话,效果就跟白苹果一样了,2倍的差距,我目前原始分辨率是 1920 * 1080,开图中的分辨率也就凑合凑合过吧

然后在 系统偏好设置 –> 通用 里面开启字体平滑(在最下面,默认也是开启的)

我对默认的平滑程度还是不太喜欢,要开最高的,终端运行以下命令,提示输入用户密码,输入完以后重启即可:

1
defaults -currentHost write -globalDomain AppleFontSmoothing -int 3

来看看效果图: 开启前: 开启后:

结束

后面有空再更更常用软件了。
然后就同意协议了,这个不用图都知道要选择什么吧… 同意协议之后就是选择硬盘了,在这个界面,没有意外的话应该可以看到我们刚刚分出来的硬盘分区:

从大学就开始折腾黑苹果,迟迟没有做些记录,总觉得差点什么,所以这次趁我需要重装系统的机会,拍了些照片,结合一些文字来做一个教程吧。 阅读按照文章顺序来,不要跳着来,除非我注明让你可以跳过。

OSX系统

不做评价谁好谁差,但是作为 Web工程师 的话,在 osx 系统下的编程体验确实要比 Windows 下好很多,软件也特别干净,从来没有在你工作的时候,突然右下角来个弹窗给你看广告,或者直接把广告就弹在最顶层来影响你的工作。 jdk npm git Dart Flutter 这些程序,在 unix 系的 osx 下也从来没有报错。所以还是推荐使用的。

安装须知

台式机

在我看来台式机安装黑果是比较完美的,能够屏蔽独立显卡,没有 WiFi 的顾虑。而且较高的性能使得能够更加完美的体验 osx 系统。 但是还是有些限制你需要知道:

  1. Intel 芯支持比较完美,如果是 AMD 芯的话,虽然说现在有破解,但是我还是觉得不如自然免驱的好(无测试过,没什么发言权);
  2. 一般比较热门的独立显卡,在国内远景论坛都有驱动的教程,涉及一些代码的注入,我比较懒,所以公司的电脑就拆掉了独显,使用集显来驱动,集显也是免驱的;

笔记本

  1. 笔记本受限比较大,现在笔记本一般都是 集显+独显 来自动切换达到省电的功能的,但是白苹果的笔记本,不是一个独显就是一个集显。所以,两者混合的情况下一般只能驱动集显(除非部分笔记本可以屏蔽集显),显卡问题就是独显基本就废了;
  2. WiFi 常年无解,两个方法解决:
    1. 淘宝购买 usb 接口的免驱网卡,卖家通常会提供使用所需要的驱动和软件;
    2. 更换笔记本内部的网卡,这一步比较专业,加上联想笔记本更换配件是需要刷白名单的,所以通常来说,都会选择第一种方式解决。

安装流程

镜像下载

因为在这篇文章产生的时候最新的 osx 系统是 10.15 catalina。所以我就是用这个系统来做教程吧。 osx_10.15_catalina 下载地址:下载地址

感谢远景论坛大佬的封装,我没有直接放大佬的最终下载地址,因为我还是希望各位能够给去下载,大佬有硬币赚。

镜像已经下好了,大佬也提供了常用的机型配置:

这一步只是先让你们看,先放一边,我们通常安装的时候不会直接选择合适的驱动,而是先使用通用驱动来安装,他是以最小的通用兼容配置来做的,通常都能够进入安装并且顺利通过。

写入优盘

此时,你需要拥有一个 16G 的优盘!记住记住,一定是 16G 不然他镜像可以刻录但是不会帮你刻录完整,不完整的镜像就是一个无法安装的镜像。(我用了一个 8G 的在这一步卡了好几个小时,最后把公司的师弟杀了祭天了,他出现了警告也不看直接点击 OK 就给我刻录了一个废盘) 首先你需要一个软件:TransMac 当然因为我的习惯,所以我都是在 Windows 系统下写优盘的。 安装,然后右键 -> 以管理员身份运行

会有提示需要购买正版什么的,不过点击 run 可以直接运行试用版,试用版就够用了。 然后找到你的优盘,右键选择 Format Disk –> Format with Disk Image 即可。

漫长的等待,结束了就可以使用这个优盘来安装系统了。

修改BIOS引导

msruefi,只能选择后者。一般新买的电脑或者近几年的电脑都是这个。 简单的识别方法:开机的时候,如果是品牌 Logo 然后就开始转圈圈进入 Windows 系统的话,就是 uefi。 第二就是修改 biossecurity boot,直接关闭就完事了。如果同等级的还有说什么引导什么系统的话,记得把它修改成 other

如果需要双系统

如果不需要双系统安装,直接跳过这一段

双系统指的是 Windows + osx,好像一般初学者都需要这个东西(包括之前的我自己)? 首先首先,Windows 默认的引导分区太小了,如果你不重新安装 Windows 的话,直接安装 osx 后面都会导致任何软件都无法安装。 教程 类似于上面地址里面的教程,但是在创建 efi 的时候,大小给大点,我一般给的是 512m,因为后面我们可能还需要根据不同的机型,在这个分区里面放入驱动。 分好区,先把 Windows 装上,记得留一些硬盘给 osx,我建议是 100G

装好系统后,在硬盘管理那里给 efi 分区随便给个盘符,然后把里面的东西备份一份出来,因为待会需要对他开刀,备份完整不至于你总是需要重新安装系统。

优盘引导开机

为了安装方便我建议将 first boot 设置为上面的优盘。当然使用 F12 来选择也行不过中途需要重启的时候就要留意重新选择了。 选择优盘中的带有 UEFI 选项的,这时候电脑将会读取优盘的引导,进入引导界面。

进入优盘是这个样子的:

一般是在第一项,下面会提示 Boot macOS Install from xxx 后面一般是优盘它默认给的名字。此时,我们 需要按一次 空格键 ,来到下一个页面:

下面是启动选项,一般按下空格需要设定的,大佬已经帮我们写好了,这个时候直接 按下回车键 进入安装软件就可以了。 一串命令后,我们会进来到这个页面:

能够来到这里,说明安装过程十分顺利,接下来进入下一个环节。

硬盘分区

就跟上面一样,我们选择磁盘工具,然后点击 继续,就会进入到磁盘工具的页面,那我们先选择显示所有设备,这时候磁盘都会显示出来。

然后我们选择整个硬盘(如果是单系统的话,双系统的话,你需要选择你要格式化的那个分区)点击上面的抹掉(看我鼠标那个位置):

点击之后会出来这个硬盘设定界面:

  1. 名称,可以自定义;

  2. 格式,新出来的文件系统是 APFS 听说是提速以及增加安全性的,那我就用这个好了,也建议使用这个。强烈不要使用区分大小写的分区格式(敲黑板),因为我使用了大小写区分的硬盘导致有些软件它不支持!!我目前就是因为这个原因重新安装系统的;

  3. 方案选择 GUID 分区图

稍稍等待一番,成功以后,直接左上角关闭磁盘管理工具。我们就可以安装系统了。

安装系统

回到这个界面,选择 安装 macOS


然后就同意协议了,这个不用图都知道要选择什么吧… 同意协议之后就是选择硬盘了,在这个界面,没有意外的话应该可以看到我们刚刚分出来的硬盘分区:

选好硬盘点击安装,接下来进入漫长的等待:

!!!请注意!!!几分钟动一次鼠标,不要让电脑进入睡眠,因为可能因为驱动原因会睡死。 然后系统在安装过程需要一次重启,别担心,如果你在之前已经设置好优盘第一启动了的话,就不用管它,没有的话,需要手动选择优盘引导,进入优盘不用做什么,会继续安装:

跑完基本就可以进入系统的基础设置页面了:

选择什么不用说了吧,选择其他国家也可以,只要看得懂就好…

完成安装

完成安装可以进入桌面了,左上角有个 -> 关于本机

哎哟卧槽,这个显存不对劲啊….. 那现在我们暂时忍一下,使用憋屈的动画效果使用一下 Safari,去下载一个软件:Clover Configurator 安装软件

LaunchPad 打开运行,不出意外的话:

LaunchPad/其他/终端 运行以下命令,输入设置的用户密码就可以了:

1
sudo spctl --master-disable

重新打开 Clover Configurator 来到这个菜单:

我们现在的引导还是优盘,所以先在优盘那一项点击后面的挂载,这时候应该需要输入用户的密码,挂载成功完以后,就可以在 访达 看到 EFI 分区了。

还记得这张图吗:

将原来的

config.plist 备份一份,把这里面的适合自己机型的配置,改名为 config.plist 放在上一层文件夹:

然后重启电脑,进入优盘引导,根据下面字样提示,选择 Boot macOS from 你设置的分区名 即可进入安装好的系统。如果配置兼容的话,重新查看关于本机:

显存正常了。 再尝试音乐,如果声卡也正常,那就完成安装了。

迁入硬盘引导

一切好了,总不能总是插着硬盘来开机吧,我们需要把硬盘的文件替换到磁盘里面去,还是使用 Clover Configurator

一个是硬盘的,一个是优盘的,同时挂载出来。 然后使用 访达 进行操作:

OK,bios 重新设置第一启动是硬盘,然后,如果需要进入 osx 则选择 osx 硬盘回车,需要 Windows 的话就选择 Windows 所在的分区即可。

其他设置

这是我自己觉得比较好的设置,每个人可以按照自己的习惯来。 当然关于原来在 Windows 习惯的 Ctrl + c 这些都需要换成 Command + C 了,Command 在原来的 Win键 上面,我是不建议在系统设置里面替换这两者的按键位置的,毕竟用起来还蛮舒服的。 然后所有软件需要进入设置界面,都已经统一是 Command + , 这个快捷键了。

访达小设置

访达默认设置我感觉是不太好用的,比如点击访达,显示的不是我的电脑的内容,而是最近使用的内容,我特么写代码最近使用的内容都是代码文件,而我们通常不需要在这里去访问这些文件的(都是通过 IDE 工具),所以我希望点击访达进来的时候就是 我的个人文件夹: 打开 访达Command + ,

第二个不好的点是,我去到那个文件夹,要搜索某个文件,默认给我跑整个电脑去搜索,速度慢又没必要,所以:

第三个不好的点是,文件默认居然不排序,还要像垃圾堆一样堆在一起!!改掉设置!: 这个不在 Command + , 里边了,在这里:

开启Retina渲染

参考资料: 黑苹果开启缩放分辨率HiDPi以及字体模糊的调整方法总结 黑苹果开启HiDPI“解决”字体模糊的问题 直接在 其他软件/终端 粘贴下面的命令运行,提示输入用户密码,输入完以后根据提示来走就可以了:

1
sh -c "$(curl -fsSL https://raw.githubusercontent.com/xzhih/one-key-hidpi/master/hidpi.sh)"

重启电脑后安装 RDM 工具来配置分辨率(下载 RDM-2.2.dmg 就可以了 ) 运行,右上角:

选择带有小闪电的就是 HiDPI 的设置(也就是 Retina 效果)

一般根据屏幕原始分辨率来定,有 4K 显示器开 2K 分辨率的话,效果就跟白苹果一样了,2倍的差距,我目前原始分辨率是 1920 * 1080,开图中的分辨率也就凑合凑合过吧

然后在 系统偏好设置 –> 通用 里面开启字体平滑(在最下面,默认也是开启的)

我对默认的平滑程度还是不太喜欢,要开最高的,终端运行以下命令,提示输入用户密码,输入完以后重启即可:

1
defaults -currentHost write -globalDomain AppleFontSmoothing -int 3

来看看效果图: 开启前: 开启后:

结束

后面有空再更更常用软件了。
选好硬盘点击安装,接下来进入漫长的等待:

从大学就开始折腾黑苹果,迟迟没有做些记录,总觉得差点什么,所以这次趁我需要重装系统的机会,拍了些照片,结合一些文字来做一个教程吧。 阅读按照文章顺序来,不要跳着来,除非我注明让你可以跳过。

OSX系统

不做评价谁好谁差,但是作为 Web工程师 的话,在 osx 系统下的编程体验确实要比 Windows 下好很多,软件也特别干净,从来没有在你工作的时候,突然右下角来个弹窗给你看广告,或者直接把广告就弹在最顶层来影响你的工作。 jdk npm git Dart Flutter 这些程序,在 unix 系的 osx 下也从来没有报错。所以还是推荐使用的。

安装须知

台式机

在我看来台式机安装黑果是比较完美的,能够屏蔽独立显卡,没有 WiFi 的顾虑。而且较高的性能使得能够更加完美的体验 osx 系统。 但是还是有些限制你需要知道:

  1. Intel 芯支持比较完美,如果是 AMD 芯的话,虽然说现在有破解,但是我还是觉得不如自然免驱的好(无测试过,没什么发言权);
  2. 一般比较热门的独立显卡,在国内远景论坛都有驱动的教程,涉及一些代码的注入,我比较懒,所以公司的电脑就拆掉了独显,使用集显来驱动,集显也是免驱的;

笔记本

  1. 笔记本受限比较大,现在笔记本一般都是 集显+独显 来自动切换达到省电的功能的,但是白苹果的笔记本,不是一个独显就是一个集显。所以,两者混合的情况下一般只能驱动集显(除非部分笔记本可以屏蔽集显),显卡问题就是独显基本就废了;
  2. WiFi 常年无解,两个方法解决:
    1. 淘宝购买 usb 接口的免驱网卡,卖家通常会提供使用所需要的驱动和软件;
    2. 更换笔记本内部的网卡,这一步比较专业,加上联想笔记本更换配件是需要刷白名单的,所以通常来说,都会选择第一种方式解决。

安装流程

镜像下载

因为在这篇文章产生的时候最新的 osx 系统是 10.15 catalina。所以我就是用这个系统来做教程吧。 osx_10.15_catalina 下载地址:下载地址

感谢远景论坛大佬的封装,我没有直接放大佬的最终下载地址,因为我还是希望各位能够给去下载,大佬有硬币赚。

镜像已经下好了,大佬也提供了常用的机型配置:

这一步只是先让你们看,先放一边,我们通常安装的时候不会直接选择合适的驱动,而是先使用通用驱动来安装,他是以最小的通用兼容配置来做的,通常都能够进入安装并且顺利通过。

写入优盘

此时,你需要拥有一个 16G 的优盘!记住记住,一定是 16G 不然他镜像可以刻录但是不会帮你刻录完整,不完整的镜像就是一个无法安装的镜像。(我用了一个 8G 的在这一步卡了好几个小时,最后把公司的师弟杀了祭天了,他出现了警告也不看直接点击 OK 就给我刻录了一个废盘) 首先你需要一个软件:TransMac 当然因为我的习惯,所以我都是在 Windows 系统下写优盘的。 安装,然后右键 -> 以管理员身份运行

会有提示需要购买正版什么的,不过点击 run 可以直接运行试用版,试用版就够用了。 然后找到你的优盘,右键选择 Format Disk –> Format with Disk Image 即可。

漫长的等待,结束了就可以使用这个优盘来安装系统了。

修改BIOS引导

msruefi,只能选择后者。一般新买的电脑或者近几年的电脑都是这个。 简单的识别方法:开机的时候,如果是品牌 Logo 然后就开始转圈圈进入 Windows 系统的话,就是 uefi。 第二就是修改 biossecurity boot,直接关闭就完事了。如果同等级的还有说什么引导什么系统的话,记得把它修改成 other

如果需要双系统

如果不需要双系统安装,直接跳过这一段

双系统指的是 Windows + osx,好像一般初学者都需要这个东西(包括之前的我自己)? 首先首先,Windows 默认的引导分区太小了,如果你不重新安装 Windows 的话,直接安装 osx 后面都会导致任何软件都无法安装。 教程 类似于上面地址里面的教程,但是在创建 efi 的时候,大小给大点,我一般给的是 512m,因为后面我们可能还需要根据不同的机型,在这个分区里面放入驱动。 分好区,先把 Windows 装上,记得留一些硬盘给 osx,我建议是 100G

装好系统后,在硬盘管理那里给 efi 分区随便给个盘符,然后把里面的东西备份一份出来,因为待会需要对他开刀,备份完整不至于你总是需要重新安装系统。

优盘引导开机

为了安装方便我建议将 first boot 设置为上面的优盘。当然使用 F12 来选择也行不过中途需要重启的时候就要留意重新选择了。 选择优盘中的带有 UEFI 选项的,这时候电脑将会读取优盘的引导,进入引导界面。

进入优盘是这个样子的:

一般是在第一项,下面会提示 Boot macOS Install from xxx 后面一般是优盘它默认给的名字。此时,我们 需要按一次 空格键 ,来到下一个页面:

下面是启动选项,一般按下空格需要设定的,大佬已经帮我们写好了,这个时候直接 按下回车键 进入安装软件就可以了。 一串命令后,我们会进来到这个页面:

能够来到这里,说明安装过程十分顺利,接下来进入下一个环节。

硬盘分区

就跟上面一样,我们选择磁盘工具,然后点击 继续,就会进入到磁盘工具的页面,那我们先选择显示所有设备,这时候磁盘都会显示出来。

然后我们选择整个硬盘(如果是单系统的话,双系统的话,你需要选择你要格式化的那个分区)点击上面的抹掉(看我鼠标那个位置):

点击之后会出来这个硬盘设定界面:

  1. 名称,可以自定义;

  2. 格式,新出来的文件系统是 APFS 听说是提速以及增加安全性的,那我就用这个好了,也建议使用这个。强烈不要使用区分大小写的分区格式(敲黑板),因为我使用了大小写区分的硬盘导致有些软件它不支持!!我目前就是因为这个原因重新安装系统的;

  3. 方案选择 GUID 分区图

稍稍等待一番,成功以后,直接左上角关闭磁盘管理工具。我们就可以安装系统了。

安装系统

回到这个界面,选择 安装 macOS


然后就同意协议了,这个不用图都知道要选择什么吧… 同意协议之后就是选择硬盘了,在这个界面,没有意外的话应该可以看到我们刚刚分出来的硬盘分区:

选好硬盘点击安装,接下来进入漫长的等待:

!!!请注意!!!几分钟动一次鼠标,不要让电脑进入睡眠,因为可能因为驱动原因会睡死。 然后系统在安装过程需要一次重启,别担心,如果你在之前已经设置好优盘第一启动了的话,就不用管它,没有的话,需要手动选择优盘引导,进入优盘不用做什么,会继续安装:

跑完基本就可以进入系统的基础设置页面了:

选择什么不用说了吧,选择其他国家也可以,只要看得懂就好…

完成安装

完成安装可以进入桌面了,左上角有个 -> 关于本机

哎哟卧槽,这个显存不对劲啊….. 那现在我们暂时忍一下,使用憋屈的动画效果使用一下 Safari,去下载一个软件:Clover Configurator 安装软件

LaunchPad 打开运行,不出意外的话:

LaunchPad/其他/终端 运行以下命令,输入设置的用户密码就可以了:

1
sudo spctl --master-disable

重新打开 Clover Configurator 来到这个菜单:

我们现在的引导还是优盘,所以先在优盘那一项点击后面的挂载,这时候应该需要输入用户的密码,挂载成功完以后,就可以在 访达 看到 EFI 分区了。

还记得这张图吗:

将原来的

config.plist 备份一份,把这里面的适合自己机型的配置,改名为 config.plist 放在上一层文件夹:

然后重启电脑,进入优盘引导,根据下面字样提示,选择 Boot macOS from 你设置的分区名 即可进入安装好的系统。如果配置兼容的话,重新查看关于本机:

显存正常了。 再尝试音乐,如果声卡也正常,那就完成安装了。

迁入硬盘引导

一切好了,总不能总是插着硬盘来开机吧,我们需要把硬盘的文件替换到磁盘里面去,还是使用 Clover Configurator

一个是硬盘的,一个是优盘的,同时挂载出来。 然后使用 访达 进行操作:

OK,bios 重新设置第一启动是硬盘,然后,如果需要进入 osx 则选择 osx 硬盘回车,需要 Windows 的话就选择 Windows 所在的分区即可。

其他设置

这是我自己觉得比较好的设置,每个人可以按照自己的习惯来。 当然关于原来在 Windows 习惯的 Ctrl + c 这些都需要换成 Command + C 了,Command 在原来的 Win键 上面,我是不建议在系统设置里面替换这两者的按键位置的,毕竟用起来还蛮舒服的。 然后所有软件需要进入设置界面,都已经统一是 Command + , 这个快捷键了。

访达小设置

访达默认设置我感觉是不太好用的,比如点击访达,显示的不是我的电脑的内容,而是最近使用的内容,我特么写代码最近使用的内容都是代码文件,而我们通常不需要在这里去访问这些文件的(都是通过 IDE 工具),所以我希望点击访达进来的时候就是 我的个人文件夹: 打开 访达Command + ,

第二个不好的点是,我去到那个文件夹,要搜索某个文件,默认给我跑整个电脑去搜索,速度慢又没必要,所以:

第三个不好的点是,文件默认居然不排序,还要像垃圾堆一样堆在一起!!改掉设置!: 这个不在 Command + , 里边了,在这里:

开启Retina渲染

参考资料: 黑苹果开启缩放分辨率HiDPi以及字体模糊的调整方法总结 黑苹果开启HiDPI“解决”字体模糊的问题 直接在 其他软件/终端 粘贴下面的命令运行,提示输入用户密码,输入完以后根据提示来走就可以了:

1
sh -c "$(curl -fsSL https://raw.githubusercontent.com/xzhih/one-key-hidpi/master/hidpi.sh)"

重启电脑后安装 RDM 工具来配置分辨率(下载 RDM-2.2.dmg 就可以了 ) 运行,右上角:

选择带有小闪电的就是 HiDPI 的设置(也就是 Retina 效果)

一般根据屏幕原始分辨率来定,有 4K 显示器开 2K 分辨率的话,效果就跟白苹果一样了,2倍的差距,我目前原始分辨率是 1920 * 1080,开图中的分辨率也就凑合凑合过吧

然后在 系统偏好设置 –> 通用 里面开启字体平滑(在最下面,默认也是开启的)

我对默认的平滑程度还是不太喜欢,要开最高的,终端运行以下命令,提示输入用户密码,输入完以后重启即可:

1
defaults -currentHost write -globalDomain AppleFontSmoothing -int 3

来看看效果图: 开启前: 开启后:

结束

后面有空再更更常用软件了。
!!!请注意!!!几分钟动一次鼠标,不要让电脑进入睡眠,因为可能因为驱动原因会睡死。 然后系统在安装过程需要一次重启,别担心,如果你在之前已经设置好优盘第一启动了的话,就不用管它,没有的话,需要手动选择优盘引导,进入优盘不用做什么,会继续安装:

从大学就开始折腾黑苹果,迟迟没有做些记录,总觉得差点什么,所以这次趁我需要重装系统的机会,拍了些照片,结合一些文字来做一个教程吧。 阅读按照文章顺序来,不要跳着来,除非我注明让你可以跳过。

OSX系统

不做评价谁好谁差,但是作为 Web工程师 的话,在 osx 系统下的编程体验确实要比 Windows 下好很多,软件也特别干净,从来没有在你工作的时候,突然右下角来个弹窗给你看广告,或者直接把广告就弹在最顶层来影响你的工作。 jdk npm git Dart Flutter 这些程序,在 unix 系的 osx 下也从来没有报错。所以还是推荐使用的。

安装须知

台式机

在我看来台式机安装黑果是比较完美的,能够屏蔽独立显卡,没有 WiFi 的顾虑。而且较高的性能使得能够更加完美的体验 osx 系统。 但是还是有些限制你需要知道:

  1. Intel 芯支持比较完美,如果是 AMD 芯的话,虽然说现在有破解,但是我还是觉得不如自然免驱的好(无测试过,没什么发言权);
  2. 一般比较热门的独立显卡,在国内远景论坛都有驱动的教程,涉及一些代码的注入,我比较懒,所以公司的电脑就拆掉了独显,使用集显来驱动,集显也是免驱的;

笔记本

  1. 笔记本受限比较大,现在笔记本一般都是 集显+独显 来自动切换达到省电的功能的,但是白苹果的笔记本,不是一个独显就是一个集显。所以,两者混合的情况下一般只能驱动集显(除非部分笔记本可以屏蔽集显),显卡问题就是独显基本就废了;
  2. WiFi 常年无解,两个方法解决:
    1. 淘宝购买 usb 接口的免驱网卡,卖家通常会提供使用所需要的驱动和软件;
    2. 更换笔记本内部的网卡,这一步比较专业,加上联想笔记本更换配件是需要刷白名单的,所以通常来说,都会选择第一种方式解决。

安装流程

镜像下载

因为在这篇文章产生的时候最新的 osx 系统是 10.15 catalina。所以我就是用这个系统来做教程吧。 osx_10.15_catalina 下载地址:下载地址

感谢远景论坛大佬的封装,我没有直接放大佬的最终下载地址,因为我还是希望各位能够给去下载,大佬有硬币赚。

镜像已经下好了,大佬也提供了常用的机型配置:

这一步只是先让你们看,先放一边,我们通常安装的时候不会直接选择合适的驱动,而是先使用通用驱动来安装,他是以最小的通用兼容配置来做的,通常都能够进入安装并且顺利通过。

写入优盘

此时,你需要拥有一个 16G 的优盘!记住记住,一定是 16G 不然他镜像可以刻录但是不会帮你刻录完整,不完整的镜像就是一个无法安装的镜像。(我用了一个 8G 的在这一步卡了好几个小时,最后把公司的师弟杀了祭天了,他出现了警告也不看直接点击 OK 就给我刻录了一个废盘) 首先你需要一个软件:TransMac 当然因为我的习惯,所以我都是在 Windows 系统下写优盘的。 安装,然后右键 -> 以管理员身份运行

会有提示需要购买正版什么的,不过点击 run 可以直接运行试用版,试用版就够用了。 然后找到你的优盘,右键选择 Format Disk –> Format with Disk Image 即可。

漫长的等待,结束了就可以使用这个优盘来安装系统了。

修改BIOS引导

msruefi,只能选择后者。一般新买的电脑或者近几年的电脑都是这个。 简单的识别方法:开机的时候,如果是品牌 Logo 然后就开始转圈圈进入 Windows 系统的话,就是 uefi。 第二就是修改 biossecurity boot,直接关闭就完事了。如果同等级的还有说什么引导什么系统的话,记得把它修改成 other

如果需要双系统

如果不需要双系统安装,直接跳过这一段

双系统指的是 Windows + osx,好像一般初学者都需要这个东西(包括之前的我自己)? 首先首先,Windows 默认的引导分区太小了,如果你不重新安装 Windows 的话,直接安装 osx 后面都会导致任何软件都无法安装。 教程 类似于上面地址里面的教程,但是在创建 efi 的时候,大小给大点,我一般给的是 512m,因为后面我们可能还需要根据不同的机型,在这个分区里面放入驱动。 分好区,先把 Windows 装上,记得留一些硬盘给 osx,我建议是 100G

装好系统后,在硬盘管理那里给 efi 分区随便给个盘符,然后把里面的东西备份一份出来,因为待会需要对他开刀,备份完整不至于你总是需要重新安装系统。

优盘引导开机

为了安装方便我建议将 first boot 设置为上面的优盘。当然使用 F12 来选择也行不过中途需要重启的时候就要留意重新选择了。 选择优盘中的带有 UEFI 选项的,这时候电脑将会读取优盘的引导,进入引导界面。

进入优盘是这个样子的:

一般是在第一项,下面会提示 Boot macOS Install from xxx 后面一般是优盘它默认给的名字。此时,我们 需要按一次 空格键 ,来到下一个页面:

下面是启动选项,一般按下空格需要设定的,大佬已经帮我们写好了,这个时候直接 按下回车键 进入安装软件就可以了。 一串命令后,我们会进来到这个页面:

能够来到这里,说明安装过程十分顺利,接下来进入下一个环节。

硬盘分区

就跟上面一样,我们选择磁盘工具,然后点击 继续,就会进入到磁盘工具的页面,那我们先选择显示所有设备,这时候磁盘都会显示出来。

然后我们选择整个硬盘(如果是单系统的话,双系统的话,你需要选择你要格式化的那个分区)点击上面的抹掉(看我鼠标那个位置):

点击之后会出来这个硬盘设定界面:

  1. 名称,可以自定义;

  2. 格式,新出来的文件系统是 APFS 听说是提速以及增加安全性的,那我就用这个好了,也建议使用这个。强烈不要使用区分大小写的分区格式(敲黑板),因为我使用了大小写区分的硬盘导致有些软件它不支持!!我目前就是因为这个原因重新安装系统的;

  3. 方案选择 GUID 分区图

稍稍等待一番,成功以后,直接左上角关闭磁盘管理工具。我们就可以安装系统了。

安装系统

回到这个界面,选择 安装 macOS


然后就同意协议了,这个不用图都知道要选择什么吧… 同意协议之后就是选择硬盘了,在这个界面,没有意外的话应该可以看到我们刚刚分出来的硬盘分区:

选好硬盘点击安装,接下来进入漫长的等待:

!!!请注意!!!几分钟动一次鼠标,不要让电脑进入睡眠,因为可能因为驱动原因会睡死。 然后系统在安装过程需要一次重启,别担心,如果你在之前已经设置好优盘第一启动了的话,就不用管它,没有的话,需要手动选择优盘引导,进入优盘不用做什么,会继续安装:

跑完基本就可以进入系统的基础设置页面了:

选择什么不用说了吧,选择其他国家也可以,只要看得懂就好…

完成安装

完成安装可以进入桌面了,左上角有个 -> 关于本机

哎哟卧槽,这个显存不对劲啊….. 那现在我们暂时忍一下,使用憋屈的动画效果使用一下 Safari,去下载一个软件:Clover Configurator 安装软件

LaunchPad 打开运行,不出意外的话:

LaunchPad/其他/终端 运行以下命令,输入设置的用户密码就可以了:

1
sudo spctl --master-disable

重新打开 Clover Configurator 来到这个菜单:

我们现在的引导还是优盘,所以先在优盘那一项点击后面的挂载,这时候应该需要输入用户的密码,挂载成功完以后,就可以在 访达 看到 EFI 分区了。

还记得这张图吗:

将原来的

config.plist 备份一份,把这里面的适合自己机型的配置,改名为 config.plist 放在上一层文件夹:

然后重启电脑,进入优盘引导,根据下面字样提示,选择 Boot macOS from 你设置的分区名 即可进入安装好的系统。如果配置兼容的话,重新查看关于本机:

显存正常了。 再尝试音乐,如果声卡也正常,那就完成安装了。

迁入硬盘引导

一切好了,总不能总是插着硬盘来开机吧,我们需要把硬盘的文件替换到磁盘里面去,还是使用 Clover Configurator

一个是硬盘的,一个是优盘的,同时挂载出来。 然后使用 访达 进行操作:

OK,bios 重新设置第一启动是硬盘,然后,如果需要进入 osx 则选择 osx 硬盘回车,需要 Windows 的话就选择 Windows 所在的分区即可。

其他设置

这是我自己觉得比较好的设置,每个人可以按照自己的习惯来。 当然关于原来在 Windows 习惯的 Ctrl + c 这些都需要换成 Command + C 了,Command 在原来的 Win键 上面,我是不建议在系统设置里面替换这两者的按键位置的,毕竟用起来还蛮舒服的。 然后所有软件需要进入设置界面,都已经统一是 Command + , 这个快捷键了。

访达小设置

访达默认设置我感觉是不太好用的,比如点击访达,显示的不是我的电脑的内容,而是最近使用的内容,我特么写代码最近使用的内容都是代码文件,而我们通常不需要在这里去访问这些文件的(都是通过 IDE 工具),所以我希望点击访达进来的时候就是 我的个人文件夹: 打开 访达Command + ,

第二个不好的点是,我去到那个文件夹,要搜索某个文件,默认给我跑整个电脑去搜索,速度慢又没必要,所以:

第三个不好的点是,文件默认居然不排序,还要像垃圾堆一样堆在一起!!改掉设置!: 这个不在 Command + , 里边了,在这里:

开启Retina渲染

参考资料: 黑苹果开启缩放分辨率HiDPi以及字体模糊的调整方法总结 黑苹果开启HiDPI“解决”字体模糊的问题 直接在 其他软件/终端 粘贴下面的命令运行,提示输入用户密码,输入完以后根据提示来走就可以了:

1
sh -c "$(curl -fsSL https://raw.githubusercontent.com/xzhih/one-key-hidpi/master/hidpi.sh)"

重启电脑后安装 RDM 工具来配置分辨率(下载 RDM-2.2.dmg 就可以了 ) 运行,右上角:

选择带有小闪电的就是 HiDPI 的设置(也就是 Retina 效果)

一般根据屏幕原始分辨率来定,有 4K 显示器开 2K 分辨率的话,效果就跟白苹果一样了,2倍的差距,我目前原始分辨率是 1920 * 1080,开图中的分辨率也就凑合凑合过吧

然后在 系统偏好设置 –> 通用 里面开启字体平滑(在最下面,默认也是开启的)

我对默认的平滑程度还是不太喜欢,要开最高的,终端运行以下命令,提示输入用户密码,输入完以后重启即可:

1
defaults -currentHost write -globalDomain AppleFontSmoothing -int 3

来看看效果图: 开启前: 开启后:

结束

后面有空再更更常用软件了。
跑完基本就可以进入系统的基础设置页面了:

从大学就开始折腾黑苹果,迟迟没有做些记录,总觉得差点什么,所以这次趁我需要重装系统的机会,拍了些照片,结合一些文字来做一个教程吧。 阅读按照文章顺序来,不要跳着来,除非我注明让你可以跳过。

OSX系统

不做评价谁好谁差,但是作为 Web工程师 的话,在 osx 系统下的编程体验确实要比 Windows 下好很多,软件也特别干净,从来没有在你工作的时候,突然右下角来个弹窗给你看广告,或者直接把广告就弹在最顶层来影响你的工作。 jdk npm git Dart Flutter 这些程序,在 unix 系的 osx 下也从来没有报错。所以还是推荐使用的。

安装须知

台式机

在我看来台式机安装黑果是比较完美的,能够屏蔽独立显卡,没有 WiFi 的顾虑。而且较高的性能使得能够更加完美的体验 osx 系统。 但是还是有些限制你需要知道:

  1. Intel 芯支持比较完美,如果是 AMD 芯的话,虽然说现在有破解,但是我还是觉得不如自然免驱的好(无测试过,没什么发言权);
  2. 一般比较热门的独立显卡,在国内远景论坛都有驱动的教程,涉及一些代码的注入,我比较懒,所以公司的电脑就拆掉了独显,使用集显来驱动,集显也是免驱的;

笔记本

  1. 笔记本受限比较大,现在笔记本一般都是 集显+独显 来自动切换达到省电的功能的,但是白苹果的笔记本,不是一个独显就是一个集显。所以,两者混合的情况下一般只能驱动集显(除非部分笔记本可以屏蔽集显),显卡问题就是独显基本就废了;
  2. WiFi 常年无解,两个方法解决:
    1. 淘宝购买 usb 接口的免驱网卡,卖家通常会提供使用所需要的驱动和软件;
    2. 更换笔记本内部的网卡,这一步比较专业,加上联想笔记本更换配件是需要刷白名单的,所以通常来说,都会选择第一种方式解决。

安装流程

镜像下载

因为在这篇文章产生的时候最新的 osx 系统是 10.15 catalina。所以我就是用这个系统来做教程吧。 osx_10.15_catalina 下载地址:下载地址

感谢远景论坛大佬的封装,我没有直接放大佬的最终下载地址,因为我还是希望各位能够给去下载,大佬有硬币赚。

镜像已经下好了,大佬也提供了常用的机型配置:

这一步只是先让你们看,先放一边,我们通常安装的时候不会直接选择合适的驱动,而是先使用通用驱动来安装,他是以最小的通用兼容配置来做的,通常都能够进入安装并且顺利通过。

写入优盘

此时,你需要拥有一个 16G 的优盘!记住记住,一定是 16G 不然他镜像可以刻录但是不会帮你刻录完整,不完整的镜像就是一个无法安装的镜像。(我用了一个 8G 的在这一步卡了好几个小时,最后把公司的师弟杀了祭天了,他出现了警告也不看直接点击 OK 就给我刻录了一个废盘) 首先你需要一个软件:TransMac 当然因为我的习惯,所以我都是在 Windows 系统下写优盘的。 安装,然后右键 -> 以管理员身份运行

会有提示需要购买正版什么的,不过点击 run 可以直接运行试用版,试用版就够用了。 然后找到你的优盘,右键选择 Format Disk –> Format with Disk Image 即可。

漫长的等待,结束了就可以使用这个优盘来安装系统了。

修改BIOS引导

msruefi,只能选择后者。一般新买的电脑或者近几年的电脑都是这个。 简单的识别方法:开机的时候,如果是品牌 Logo 然后就开始转圈圈进入 Windows 系统的话,就是 uefi。 第二就是修改 biossecurity boot,直接关闭就完事了。如果同等级的还有说什么引导什么系统的话,记得把它修改成 other

如果需要双系统

如果不需要双系统安装,直接跳过这一段

双系统指的是 Windows + osx,好像一般初学者都需要这个东西(包括之前的我自己)? 首先首先,Windows 默认的引导分区太小了,如果你不重新安装 Windows 的话,直接安装 osx 后面都会导致任何软件都无法安装。 教程 类似于上面地址里面的教程,但是在创建 efi 的时候,大小给大点,我一般给的是 512m,因为后面我们可能还需要根据不同的机型,在这个分区里面放入驱动。 分好区,先把 Windows 装上,记得留一些硬盘给 osx,我建议是 100G

装好系统后,在硬盘管理那里给 efi 分区随便给个盘符,然后把里面的东西备份一份出来,因为待会需要对他开刀,备份完整不至于你总是需要重新安装系统。

优盘引导开机

为了安装方便我建议将 first boot 设置为上面的优盘。当然使用 F12 来选择也行不过中途需要重启的时候就要留意重新选择了。 选择优盘中的带有 UEFI 选项的,这时候电脑将会读取优盘的引导,进入引导界面。

进入优盘是这个样子的:

一般是在第一项,下面会提示 Boot macOS Install from xxx 后面一般是优盘它默认给的名字。此时,我们 需要按一次 空格键 ,来到下一个页面:

下面是启动选项,一般按下空格需要设定的,大佬已经帮我们写好了,这个时候直接 按下回车键 进入安装软件就可以了。 一串命令后,我们会进来到这个页面:

能够来到这里,说明安装过程十分顺利,接下来进入下一个环节。

硬盘分区

就跟上面一样,我们选择磁盘工具,然后点击 继续,就会进入到磁盘工具的页面,那我们先选择显示所有设备,这时候磁盘都会显示出来。

然后我们选择整个硬盘(如果是单系统的话,双系统的话,你需要选择你要格式化的那个分区)点击上面的抹掉(看我鼠标那个位置):

点击之后会出来这个硬盘设定界面:

  1. 名称,可以自定义;

  2. 格式,新出来的文件系统是 APFS 听说是提速以及增加安全性的,那我就用这个好了,也建议使用这个。强烈不要使用区分大小写的分区格式(敲黑板),因为我使用了大小写区分的硬盘导致有些软件它不支持!!我目前就是因为这个原因重新安装系统的;

  3. 方案选择 GUID 分区图

稍稍等待一番,成功以后,直接左上角关闭磁盘管理工具。我们就可以安装系统了。

安装系统

回到这个界面,选择 安装 macOS


然后就同意协议了,这个不用图都知道要选择什么吧… 同意协议之后就是选择硬盘了,在这个界面,没有意外的话应该可以看到我们刚刚分出来的硬盘分区:

选好硬盘点击安装,接下来进入漫长的等待:

!!!请注意!!!几分钟动一次鼠标,不要让电脑进入睡眠,因为可能因为驱动原因会睡死。 然后系统在安装过程需要一次重启,别担心,如果你在之前已经设置好优盘第一启动了的话,就不用管它,没有的话,需要手动选择优盘引导,进入优盘不用做什么,会继续安装:

跑完基本就可以进入系统的基础设置页面了:

选择什么不用说了吧,选择其他国家也可以,只要看得懂就好…

完成安装

完成安装可以进入桌面了,左上角有个 -> 关于本机

哎哟卧槽,这个显存不对劲啊….. 那现在我们暂时忍一下,使用憋屈的动画效果使用一下 Safari,去下载一个软件:Clover Configurator 安装软件

LaunchPad 打开运行,不出意外的话:

LaunchPad/其他/终端 运行以下命令,输入设置的用户密码就可以了:

1
sudo spctl --master-disable

重新打开 Clover Configurator 来到这个菜单:

我们现在的引导还是优盘,所以先在优盘那一项点击后面的挂载,这时候应该需要输入用户的密码,挂载成功完以后,就可以在 访达 看到 EFI 分区了。

还记得这张图吗:

将原来的

config.plist 备份一份,把这里面的适合自己机型的配置,改名为 config.plist 放在上一层文件夹:

然后重启电脑,进入优盘引导,根据下面字样提示,选择 Boot macOS from 你设置的分区名 即可进入安装好的系统。如果配置兼容的话,重新查看关于本机:

显存正常了。 再尝试音乐,如果声卡也正常,那就完成安装了。

迁入硬盘引导

一切好了,总不能总是插着硬盘来开机吧,我们需要把硬盘的文件替换到磁盘里面去,还是使用 Clover Configurator

一个是硬盘的,一个是优盘的,同时挂载出来。 然后使用 访达 进行操作:

OK,bios 重新设置第一启动是硬盘,然后,如果需要进入 osx 则选择 osx 硬盘回车,需要 Windows 的话就选择 Windows 所在的分区即可。

其他设置

这是我自己觉得比较好的设置,每个人可以按照自己的习惯来。 当然关于原来在 Windows 习惯的 Ctrl + c 这些都需要换成 Command + C 了,Command 在原来的 Win键 上面,我是不建议在系统设置里面替换这两者的按键位置的,毕竟用起来还蛮舒服的。 然后所有软件需要进入设置界面,都已经统一是 Command + , 这个快捷键了。

访达小设置

访达默认设置我感觉是不太好用的,比如点击访达,显示的不是我的电脑的内容,而是最近使用的内容,我特么写代码最近使用的内容都是代码文件,而我们通常不需要在这里去访问这些文件的(都是通过 IDE 工具),所以我希望点击访达进来的时候就是 我的个人文件夹: 打开 访达Command + ,

第二个不好的点是,我去到那个文件夹,要搜索某个文件,默认给我跑整个电脑去搜索,速度慢又没必要,所以:

第三个不好的点是,文件默认居然不排序,还要像垃圾堆一样堆在一起!!改掉设置!: 这个不在 Command + , 里边了,在这里:

开启Retina渲染

参考资料: 黑苹果开启缩放分辨率HiDPi以及字体模糊的调整方法总结 黑苹果开启HiDPI“解决”字体模糊的问题 直接在 其他软件/终端 粘贴下面的命令运行,提示输入用户密码,输入完以后根据提示来走就可以了:

1
sh -c "$(curl -fsSL https://raw.githubusercontent.com/xzhih/one-key-hidpi/master/hidpi.sh)"

重启电脑后安装 RDM 工具来配置分辨率(下载 RDM-2.2.dmg 就可以了 ) 运行,右上角:

选择带有小闪电的就是 HiDPI 的设置(也就是 Retina 效果)

一般根据屏幕原始分辨率来定,有 4K 显示器开 2K 分辨率的话,效果就跟白苹果一样了,2倍的差距,我目前原始分辨率是 1920 * 1080,开图中的分辨率也就凑合凑合过吧

然后在 系统偏好设置 –> 通用 里面开启字体平滑(在最下面,默认也是开启的)

我对默认的平滑程度还是不太喜欢,要开最高的,终端运行以下命令,提示输入用户密码,输入完以后重启即可:

1
defaults -currentHost write -globalDomain AppleFontSmoothing -int 3

来看看效果图: 开启前: 开启后:

结束

后面有空再更更常用软件了。
选择什么不用说了吧,选择其他国家也可以,只要看得懂就好…

完成安装

完成安装可以进入桌面了,左上角有个 -> 关于本机

从大学就开始折腾黑苹果,迟迟没有做些记录,总觉得差点什么,所以这次趁我需要重装系统的机会,拍了些照片,结合一些文字来做一个教程吧。 阅读按照文章顺序来,不要跳着来,除非我注明让你可以跳过。

OSX系统

不做评价谁好谁差,但是作为 Web工程师 的话,在 osx 系统下的编程体验确实要比 Windows 下好很多,软件也特别干净,从来没有在你工作的时候,突然右下角来个弹窗给你看广告,或者直接把广告就弹在最顶层来影响你的工作。 jdk npm git Dart Flutter 这些程序,在 unix 系的 osx 下也从来没有报错。所以还是推荐使用的。

安装须知

台式机

在我看来台式机安装黑果是比较完美的,能够屏蔽独立显卡,没有 WiFi 的顾虑。而且较高的性能使得能够更加完美的体验 osx 系统。 但是还是有些限制你需要知道:

  1. Intel 芯支持比较完美,如果是 AMD 芯的话,虽然说现在有破解,但是我还是觉得不如自然免驱的好(无测试过,没什么发言权);
  2. 一般比较热门的独立显卡,在国内远景论坛都有驱动的教程,涉及一些代码的注入,我比较懒,所以公司的电脑就拆掉了独显,使用集显来驱动,集显也是免驱的;

笔记本

  1. 笔记本受限比较大,现在笔记本一般都是 集显+独显 来自动切换达到省电的功能的,但是白苹果的笔记本,不是一个独显就是一个集显。所以,两者混合的情况下一般只能驱动集显(除非部分笔记本可以屏蔽集显),显卡问题就是独显基本就废了;
  2. WiFi 常年无解,两个方法解决:
    1. 淘宝购买 usb 接口的免驱网卡,卖家通常会提供使用所需要的驱动和软件;
    2. 更换笔记本内部的网卡,这一步比较专业,加上联想笔记本更换配件是需要刷白名单的,所以通常来说,都会选择第一种方式解决。

安装流程

镜像下载

因为在这篇文章产生的时候最新的 osx 系统是 10.15 catalina。所以我就是用这个系统来做教程吧。 osx_10.15_catalina 下载地址:下载地址

感谢远景论坛大佬的封装,我没有直接放大佬的最终下载地址,因为我还是希望各位能够给去下载,大佬有硬币赚。

镜像已经下好了,大佬也提供了常用的机型配置:

这一步只是先让你们看,先放一边,我们通常安装的时候不会直接选择合适的驱动,而是先使用通用驱动来安装,他是以最小的通用兼容配置来做的,通常都能够进入安装并且顺利通过。

写入优盘

此时,你需要拥有一个 16G 的优盘!记住记住,一定是 16G 不然他镜像可以刻录但是不会帮你刻录完整,不完整的镜像就是一个无法安装的镜像。(我用了一个 8G 的在这一步卡了好几个小时,最后把公司的师弟杀了祭天了,他出现了警告也不看直接点击 OK 就给我刻录了一个废盘) 首先你需要一个软件:TransMac 当然因为我的习惯,所以我都是在 Windows 系统下写优盘的。 安装,然后右键 -> 以管理员身份运行

会有提示需要购买正版什么的,不过点击 run 可以直接运行试用版,试用版就够用了。 然后找到你的优盘,右键选择 Format Disk –> Format with Disk Image 即可。

漫长的等待,结束了就可以使用这个优盘来安装系统了。

修改BIOS引导

msruefi,只能选择后者。一般新买的电脑或者近几年的电脑都是这个。 简单的识别方法:开机的时候,如果是品牌 Logo 然后就开始转圈圈进入 Windows 系统的话,就是 uefi。 第二就是修改 biossecurity boot,直接关闭就完事了。如果同等级的还有说什么引导什么系统的话,记得把它修改成 other

如果需要双系统

如果不需要双系统安装,直接跳过这一段

双系统指的是 Windows + osx,好像一般初学者都需要这个东西(包括之前的我自己)? 首先首先,Windows 默认的引导分区太小了,如果你不重新安装 Windows 的话,直接安装 osx 后面都会导致任何软件都无法安装。 教程 类似于上面地址里面的教程,但是在创建 efi 的时候,大小给大点,我一般给的是 512m,因为后面我们可能还需要根据不同的机型,在这个分区里面放入驱动。 分好区,先把 Windows 装上,记得留一些硬盘给 osx,我建议是 100G

装好系统后,在硬盘管理那里给 efi 分区随便给个盘符,然后把里面的东西备份一份出来,因为待会需要对他开刀,备份完整不至于你总是需要重新安装系统。

优盘引导开机

为了安装方便我建议将 first boot 设置为上面的优盘。当然使用 F12 来选择也行不过中途需要重启的时候就要留意重新选择了。 选择优盘中的带有 UEFI 选项的,这时候电脑将会读取优盘的引导,进入引导界面。

进入优盘是这个样子的:

一般是在第一项,下面会提示 Boot macOS Install from xxx 后面一般是优盘它默认给的名字。此时,我们 需要按一次 空格键 ,来到下一个页面:

下面是启动选项,一般按下空格需要设定的,大佬已经帮我们写好了,这个时候直接 按下回车键 进入安装软件就可以了。 一串命令后,我们会进来到这个页面:

能够来到这里,说明安装过程十分顺利,接下来进入下一个环节。

硬盘分区

就跟上面一样,我们选择磁盘工具,然后点击 继续,就会进入到磁盘工具的页面,那我们先选择显示所有设备,这时候磁盘都会显示出来。

然后我们选择整个硬盘(如果是单系统的话,双系统的话,你需要选择你要格式化的那个分区)点击上面的抹掉(看我鼠标那个位置):

点击之后会出来这个硬盘设定界面:

  1. 名称,可以自定义;

  2. 格式,新出来的文件系统是 APFS 听说是提速以及增加安全性的,那我就用这个好了,也建议使用这个。强烈不要使用区分大小写的分区格式(敲黑板),因为我使用了大小写区分的硬盘导致有些软件它不支持!!我目前就是因为这个原因重新安装系统的;

  3. 方案选择 GUID 分区图

稍稍等待一番,成功以后,直接左上角关闭磁盘管理工具。我们就可以安装系统了。

安装系统

回到这个界面,选择 安装 macOS


然后就同意协议了,这个不用图都知道要选择什么吧… 同意协议之后就是选择硬盘了,在这个界面,没有意外的话应该可以看到我们刚刚分出来的硬盘分区:

选好硬盘点击安装,接下来进入漫长的等待:

!!!请注意!!!几分钟动一次鼠标,不要让电脑进入睡眠,因为可能因为驱动原因会睡死。 然后系统在安装过程需要一次重启,别担心,如果你在之前已经设置好优盘第一启动了的话,就不用管它,没有的话,需要手动选择优盘引导,进入优盘不用做什么,会继续安装:

跑完基本就可以进入系统的基础设置页面了:

选择什么不用说了吧,选择其他国家也可以,只要看得懂就好…

完成安装

完成安装可以进入桌面了,左上角有个 -> 关于本机

哎哟卧槽,这个显存不对劲啊….. 那现在我们暂时忍一下,使用憋屈的动画效果使用一下 Safari,去下载一个软件:Clover Configurator 安装软件

LaunchPad 打开运行,不出意外的话:

LaunchPad/其他/终端 运行以下命令,输入设置的用户密码就可以了:

1
sudo spctl --master-disable

重新打开 Clover Configurator 来到这个菜单:

我们现在的引导还是优盘,所以先在优盘那一项点击后面的挂载,这时候应该需要输入用户的密码,挂载成功完以后,就可以在 访达 看到 EFI 分区了。

还记得这张图吗:

将原来的

config.plist 备份一份,把这里面的适合自己机型的配置,改名为 config.plist 放在上一层文件夹:

然后重启电脑,进入优盘引导,根据下面字样提示,选择 Boot macOS from 你设置的分区名 即可进入安装好的系统。如果配置兼容的话,重新查看关于本机:

显存正常了。 再尝试音乐,如果声卡也正常,那就完成安装了。

迁入硬盘引导

一切好了,总不能总是插着硬盘来开机吧,我们需要把硬盘的文件替换到磁盘里面去,还是使用 Clover Configurator

一个是硬盘的,一个是优盘的,同时挂载出来。 然后使用 访达 进行操作:

OK,bios 重新设置第一启动是硬盘,然后,如果需要进入 osx 则选择 osx 硬盘回车,需要 Windows 的话就选择 Windows 所在的分区即可。

其他设置

这是我自己觉得比较好的设置,每个人可以按照自己的习惯来。 当然关于原来在 Windows 习惯的 Ctrl + c 这些都需要换成 Command + C 了,Command 在原来的 Win键 上面,我是不建议在系统设置里面替换这两者的按键位置的,毕竟用起来还蛮舒服的。 然后所有软件需要进入设置界面,都已经统一是 Command + , 这个快捷键了。

访达小设置

访达默认设置我感觉是不太好用的,比如点击访达,显示的不是我的电脑的内容,而是最近使用的内容,我特么写代码最近使用的内容都是代码文件,而我们通常不需要在这里去访问这些文件的(都是通过 IDE 工具),所以我希望点击访达进来的时候就是 我的个人文件夹: 打开 访达Command + ,

第二个不好的点是,我去到那个文件夹,要搜索某个文件,默认给我跑整个电脑去搜索,速度慢又没必要,所以:

第三个不好的点是,文件默认居然不排序,还要像垃圾堆一样堆在一起!!改掉设置!: 这个不在 Command + , 里边了,在这里:

开启Retina渲染

参考资料: 黑苹果开启缩放分辨率HiDPi以及字体模糊的调整方法总结 黑苹果开启HiDPI“解决”字体模糊的问题 直接在 其他软件/终端 粘贴下面的命令运行,提示输入用户密码,输入完以后根据提示来走就可以了:

1
sh -c "$(curl -fsSL https://raw.githubusercontent.com/xzhih/one-key-hidpi/master/hidpi.sh)"

重启电脑后安装 RDM 工具来配置分辨率(下载 RDM-2.2.dmg 就可以了 ) 运行,右上角:

选择带有小闪电的就是 HiDPI 的设置(也就是 Retina 效果)

一般根据屏幕原始分辨率来定,有 4K 显示器开 2K 分辨率的话,效果就跟白苹果一样了,2倍的差距,我目前原始分辨率是 1920 * 1080,开图中的分辨率也就凑合凑合过吧

然后在 系统偏好设置 –> 通用 里面开启字体平滑(在最下面,默认也是开启的)

我对默认的平滑程度还是不太喜欢,要开最高的,终端运行以下命令,提示输入用户密码,输入完以后重启即可:

1
defaults -currentHost write -globalDomain AppleFontSmoothing -int 3

来看看效果图: 开启前: 开启后:

结束

后面有空再更更常用软件了。

哎哟卧槽,这个显存不对劲啊….. 那现在我们暂时忍一下,使用憋屈的动画效果使用一下 Safari,去下载一个软件:Clover Configurator 安装软件

从大学就开始折腾黑苹果,迟迟没有做些记录,总觉得差点什么,所以这次趁我需要重装系统的机会,拍了些照片,结合一些文字来做一个教程吧。 阅读按照文章顺序来,不要跳着来,除非我注明让你可以跳过。

OSX系统

不做评价谁好谁差,但是作为 Web工程师 的话,在 osx 系统下的编程体验确实要比 Windows 下好很多,软件也特别干净,从来没有在你工作的时候,突然右下角来个弹窗给你看广告,或者直接把广告就弹在最顶层来影响你的工作。 jdk npm git Dart Flutter 这些程序,在 unix 系的 osx 下也从来没有报错。所以还是推荐使用的。

安装须知

台式机

在我看来台式机安装黑果是比较完美的,能够屏蔽独立显卡,没有 WiFi 的顾虑。而且较高的性能使得能够更加完美的体验 osx 系统。 但是还是有些限制你需要知道:

  1. Intel 芯支持比较完美,如果是 AMD 芯的话,虽然说现在有破解,但是我还是觉得不如自然免驱的好(无测试过,没什么发言权);
  2. 一般比较热门的独立显卡,在国内远景论坛都有驱动的教程,涉及一些代码的注入,我比较懒,所以公司的电脑就拆掉了独显,使用集显来驱动,集显也是免驱的;

笔记本

  1. 笔记本受限比较大,现在笔记本一般都是 集显+独显 来自动切换达到省电的功能的,但是白苹果的笔记本,不是一个独显就是一个集显。所以,两者混合的情况下一般只能驱动集显(除非部分笔记本可以屏蔽集显),显卡问题就是独显基本就废了;
  2. WiFi 常年无解,两个方法解决:
    1. 淘宝购买 usb 接口的免驱网卡,卖家通常会提供使用所需要的驱动和软件;
    2. 更换笔记本内部的网卡,这一步比较专业,加上联想笔记本更换配件是需要刷白名单的,所以通常来说,都会选择第一种方式解决。

安装流程

镜像下载

因为在这篇文章产生的时候最新的 osx 系统是 10.15 catalina。所以我就是用这个系统来做教程吧。 osx_10.15_catalina 下载地址:下载地址

感谢远景论坛大佬的封装,我没有直接放大佬的最终下载地址,因为我还是希望各位能够给去下载,大佬有硬币赚。

镜像已经下好了,大佬也提供了常用的机型配置:

这一步只是先让你们看,先放一边,我们通常安装的时候不会直接选择合适的驱动,而是先使用通用驱动来安装,他是以最小的通用兼容配置来做的,通常都能够进入安装并且顺利通过。

写入优盘

此时,你需要拥有一个 16G 的优盘!记住记住,一定是 16G 不然他镜像可以刻录但是不会帮你刻录完整,不完整的镜像就是一个无法安装的镜像。(我用了一个 8G 的在这一步卡了好几个小时,最后把公司的师弟杀了祭天了,他出现了警告也不看直接点击 OK 就给我刻录了一个废盘) 首先你需要一个软件:TransMac 当然因为我的习惯,所以我都是在 Windows 系统下写优盘的。 安装,然后右键 -> 以管理员身份运行

会有提示需要购买正版什么的,不过点击 run 可以直接运行试用版,试用版就够用了。 然后找到你的优盘,右键选择 Format Disk –> Format with Disk Image 即可。

漫长的等待,结束了就可以使用这个优盘来安装系统了。

修改BIOS引导

msruefi,只能选择后者。一般新买的电脑或者近几年的电脑都是这个。 简单的识别方法:开机的时候,如果是品牌 Logo 然后就开始转圈圈进入 Windows 系统的话,就是 uefi。 第二就是修改 biossecurity boot,直接关闭就完事了。如果同等级的还有说什么引导什么系统的话,记得把它修改成 other

如果需要双系统

如果不需要双系统安装,直接跳过这一段

双系统指的是 Windows + osx,好像一般初学者都需要这个东西(包括之前的我自己)? 首先首先,Windows 默认的引导分区太小了,如果你不重新安装 Windows 的话,直接安装 osx 后面都会导致任何软件都无法安装。 教程 类似于上面地址里面的教程,但是在创建 efi 的时候,大小给大点,我一般给的是 512m,因为后面我们可能还需要根据不同的机型,在这个分区里面放入驱动。 分好区,先把 Windows 装上,记得留一些硬盘给 osx,我建议是 100G

装好系统后,在硬盘管理那里给 efi 分区随便给个盘符,然后把里面的东西备份一份出来,因为待会需要对他开刀,备份完整不至于你总是需要重新安装系统。

优盘引导开机

为了安装方便我建议将 first boot 设置为上面的优盘。当然使用 F12 来选择也行不过中途需要重启的时候就要留意重新选择了。 选择优盘中的带有 UEFI 选项的,这时候电脑将会读取优盘的引导,进入引导界面。

进入优盘是这个样子的:

一般是在第一项,下面会提示 Boot macOS Install from xxx 后面一般是优盘它默认给的名字。此时,我们 需要按一次 空格键 ,来到下一个页面:

下面是启动选项,一般按下空格需要设定的,大佬已经帮我们写好了,这个时候直接 按下回车键 进入安装软件就可以了。 一串命令后,我们会进来到这个页面:

能够来到这里,说明安装过程十分顺利,接下来进入下一个环节。

硬盘分区

就跟上面一样,我们选择磁盘工具,然后点击 继续,就会进入到磁盘工具的页面,那我们先选择显示所有设备,这时候磁盘都会显示出来。

然后我们选择整个硬盘(如果是单系统的话,双系统的话,你需要选择你要格式化的那个分区)点击上面的抹掉(看我鼠标那个位置):

点击之后会出来这个硬盘设定界面:

  1. 名称,可以自定义;

  2. 格式,新出来的文件系统是 APFS 听说是提速以及增加安全性的,那我就用这个好了,也建议使用这个。强烈不要使用区分大小写的分区格式(敲黑板),因为我使用了大小写区分的硬盘导致有些软件它不支持!!我目前就是因为这个原因重新安装系统的;

  3. 方案选择 GUID 分区图

稍稍等待一番,成功以后,直接左上角关闭磁盘管理工具。我们就可以安装系统了。

安装系统

回到这个界面,选择 安装 macOS


然后就同意协议了,这个不用图都知道要选择什么吧… 同意协议之后就是选择硬盘了,在这个界面,没有意外的话应该可以看到我们刚刚分出来的硬盘分区:

选好硬盘点击安装,接下来进入漫长的等待:

!!!请注意!!!几分钟动一次鼠标,不要让电脑进入睡眠,因为可能因为驱动原因会睡死。 然后系统在安装过程需要一次重启,别担心,如果你在之前已经设置好优盘第一启动了的话,就不用管它,没有的话,需要手动选择优盘引导,进入优盘不用做什么,会继续安装:

跑完基本就可以进入系统的基础设置页面了:

选择什么不用说了吧,选择其他国家也可以,只要看得懂就好…

完成安装

完成安装可以进入桌面了,左上角有个 -> 关于本机

哎哟卧槽,这个显存不对劲啊….. 那现在我们暂时忍一下,使用憋屈的动画效果使用一下 Safari,去下载一个软件:Clover Configurator 安装软件

LaunchPad 打开运行,不出意外的话:

LaunchPad/其他/终端 运行以下命令,输入设置的用户密码就可以了:

1
sudo spctl --master-disable

重新打开 Clover Configurator 来到这个菜单:

我们现在的引导还是优盘,所以先在优盘那一项点击后面的挂载,这时候应该需要输入用户的密码,挂载成功完以后,就可以在 访达 看到 EFI 分区了。

还记得这张图吗:

将原来的

config.plist 备份一份,把这里面的适合自己机型的配置,改名为 config.plist 放在上一层文件夹:

然后重启电脑,进入优盘引导,根据下面字样提示,选择 Boot macOS from 你设置的分区名 即可进入安装好的系统。如果配置兼容的话,重新查看关于本机:

显存正常了。 再尝试音乐,如果声卡也正常,那就完成安装了。

迁入硬盘引导

一切好了,总不能总是插着硬盘来开机吧,我们需要把硬盘的文件替换到磁盘里面去,还是使用 Clover Configurator

一个是硬盘的,一个是优盘的,同时挂载出来。 然后使用 访达 进行操作:

OK,bios 重新设置第一启动是硬盘,然后,如果需要进入 osx 则选择 osx 硬盘回车,需要 Windows 的话就选择 Windows 所在的分区即可。

其他设置

这是我自己觉得比较好的设置,每个人可以按照自己的习惯来。 当然关于原来在 Windows 习惯的 Ctrl + c 这些都需要换成 Command + C 了,Command 在原来的 Win键 上面,我是不建议在系统设置里面替换这两者的按键位置的,毕竟用起来还蛮舒服的。 然后所有软件需要进入设置界面,都已经统一是 Command + , 这个快捷键了。

访达小设置

访达默认设置我感觉是不太好用的,比如点击访达,显示的不是我的电脑的内容,而是最近使用的内容,我特么写代码最近使用的内容都是代码文件,而我们通常不需要在这里去访问这些文件的(都是通过 IDE 工具),所以我希望点击访达进来的时候就是 我的个人文件夹: 打开 访达Command + ,

第二个不好的点是,我去到那个文件夹,要搜索某个文件,默认给我跑整个电脑去搜索,速度慢又没必要,所以:

第三个不好的点是,文件默认居然不排序,还要像垃圾堆一样堆在一起!!改掉设置!: 这个不在 Command + , 里边了,在这里:

开启Retina渲染

参考资料: 黑苹果开启缩放分辨率HiDPi以及字体模糊的调整方法总结 黑苹果开启HiDPI“解决”字体模糊的问题 直接在 其他软件/终端 粘贴下面的命令运行,提示输入用户密码,输入完以后根据提示来走就可以了:

1
sh -c "$(curl -fsSL https://raw.githubusercontent.com/xzhih/one-key-hidpi/master/hidpi.sh)"

重启电脑后安装 RDM 工具来配置分辨率(下载 RDM-2.2.dmg 就可以了 ) 运行,右上角:

选择带有小闪电的就是 HiDPI 的设置(也就是 Retina 效果)

一般根据屏幕原始分辨率来定,有 4K 显示器开 2K 分辨率的话,效果就跟白苹果一样了,2倍的差距,我目前原始分辨率是 1920 * 1080,开图中的分辨率也就凑合凑合过吧

然后在 系统偏好设置 –> 通用 里面开启字体平滑(在最下面,默认也是开启的)

我对默认的平滑程度还是不太喜欢,要开最高的,终端运行以下命令,提示输入用户密码,输入完以后重启即可:

1
defaults -currentHost write -globalDomain AppleFontSmoothing -int 3

来看看效果图: 开启前: 开启后:

结束

后面有空再更更常用软件了。

LaunchPad 打开运行,不出意外的话:

从大学就开始折腾黑苹果,迟迟没有做些记录,总觉得差点什么,所以这次趁我需要重装系统的机会,拍了些照片,结合一些文字来做一个教程吧。 阅读按照文章顺序来,不要跳着来,除非我注明让你可以跳过。

OSX系统

不做评价谁好谁差,但是作为 Web工程师 的话,在 osx 系统下的编程体验确实要比 Windows 下好很多,软件也特别干净,从来没有在你工作的时候,突然右下角来个弹窗给你看广告,或者直接把广告就弹在最顶层来影响你的工作。 jdk npm git Dart Flutter 这些程序,在 unix 系的 osx 下也从来没有报错。所以还是推荐使用的。

安装须知

台式机

在我看来台式机安装黑果是比较完美的,能够屏蔽独立显卡,没有 WiFi 的顾虑。而且较高的性能使得能够更加完美的体验 osx 系统。 但是还是有些限制你需要知道:

  1. Intel 芯支持比较完美,如果是 AMD 芯的话,虽然说现在有破解,但是我还是觉得不如自然免驱的好(无测试过,没什么发言权);
  2. 一般比较热门的独立显卡,在国内远景论坛都有驱动的教程,涉及一些代码的注入,我比较懒,所以公司的电脑就拆掉了独显,使用集显来驱动,集显也是免驱的;

笔记本

  1. 笔记本受限比较大,现在笔记本一般都是 集显+独显 来自动切换达到省电的功能的,但是白苹果的笔记本,不是一个独显就是一个集显。所以,两者混合的情况下一般只能驱动集显(除非部分笔记本可以屏蔽集显),显卡问题就是独显基本就废了;
  2. WiFi 常年无解,两个方法解决:
    1. 淘宝购买 usb 接口的免驱网卡,卖家通常会提供使用所需要的驱动和软件;
    2. 更换笔记本内部的网卡,这一步比较专业,加上联想笔记本更换配件是需要刷白名单的,所以通常来说,都会选择第一种方式解决。

安装流程

镜像下载

因为在这篇文章产生的时候最新的 osx 系统是 10.15 catalina。所以我就是用这个系统来做教程吧。 osx_10.15_catalina 下载地址:下载地址

感谢远景论坛大佬的封装,我没有直接放大佬的最终下载地址,因为我还是希望各位能够给去下载,大佬有硬币赚。

镜像已经下好了,大佬也提供了常用的机型配置:

这一步只是先让你们看,先放一边,我们通常安装的时候不会直接选择合适的驱动,而是先使用通用驱动来安装,他是以最小的通用兼容配置来做的,通常都能够进入安装并且顺利通过。

写入优盘

此时,你需要拥有一个 16G 的优盘!记住记住,一定是 16G 不然他镜像可以刻录但是不会帮你刻录完整,不完整的镜像就是一个无法安装的镜像。(我用了一个 8G 的在这一步卡了好几个小时,最后把公司的师弟杀了祭天了,他出现了警告也不看直接点击 OK 就给我刻录了一个废盘) 首先你需要一个软件:TransMac 当然因为我的习惯,所以我都是在 Windows 系统下写优盘的。 安装,然后右键 -> 以管理员身份运行

会有提示需要购买正版什么的,不过点击 run 可以直接运行试用版,试用版就够用了。 然后找到你的优盘,右键选择 Format Disk –> Format with Disk Image 即可。

漫长的等待,结束了就可以使用这个优盘来安装系统了。

修改BIOS引导

msruefi,只能选择后者。一般新买的电脑或者近几年的电脑都是这个。 简单的识别方法:开机的时候,如果是品牌 Logo 然后就开始转圈圈进入 Windows 系统的话,就是 uefi。 第二就是修改 biossecurity boot,直接关闭就完事了。如果同等级的还有说什么引导什么系统的话,记得把它修改成 other

如果需要双系统

如果不需要双系统安装,直接跳过这一段

双系统指的是 Windows + osx,好像一般初学者都需要这个东西(包括之前的我自己)? 首先首先,Windows 默认的引导分区太小了,如果你不重新安装 Windows 的话,直接安装 osx 后面都会导致任何软件都无法安装。 教程 类似于上面地址里面的教程,但是在创建 efi 的时候,大小给大点,我一般给的是 512m,因为后面我们可能还需要根据不同的机型,在这个分区里面放入驱动。 分好区,先把 Windows 装上,记得留一些硬盘给 osx,我建议是 100G

装好系统后,在硬盘管理那里给 efi 分区随便给个盘符,然后把里面的东西备份一份出来,因为待会需要对他开刀,备份完整不至于你总是需要重新安装系统。

优盘引导开机

为了安装方便我建议将 first boot 设置为上面的优盘。当然使用 F12 来选择也行不过中途需要重启的时候就要留意重新选择了。 选择优盘中的带有 UEFI 选项的,这时候电脑将会读取优盘的引导,进入引导界面。

进入优盘是这个样子的:

一般是在第一项,下面会提示 Boot macOS Install from xxx 后面一般是优盘它默认给的名字。此时,我们 需要按一次 空格键 ,来到下一个页面:

下面是启动选项,一般按下空格需要设定的,大佬已经帮我们写好了,这个时候直接 按下回车键 进入安装软件就可以了。 一串命令后,我们会进来到这个页面:

能够来到这里,说明安装过程十分顺利,接下来进入下一个环节。

硬盘分区

就跟上面一样,我们选择磁盘工具,然后点击 继续,就会进入到磁盘工具的页面,那我们先选择显示所有设备,这时候磁盘都会显示出来。

然后我们选择整个硬盘(如果是单系统的话,双系统的话,你需要选择你要格式化的那个分区)点击上面的抹掉(看我鼠标那个位置):

点击之后会出来这个硬盘设定界面:

  1. 名称,可以自定义;

  2. 格式,新出来的文件系统是 APFS 听说是提速以及增加安全性的,那我就用这个好了,也建议使用这个。强烈不要使用区分大小写的分区格式(敲黑板),因为我使用了大小写区分的硬盘导致有些软件它不支持!!我目前就是因为这个原因重新安装系统的;

  3. 方案选择 GUID 分区图

稍稍等待一番,成功以后,直接左上角关闭磁盘管理工具。我们就可以安装系统了。

安装系统

回到这个界面,选择 安装 macOS


然后就同意协议了,这个不用图都知道要选择什么吧… 同意协议之后就是选择硬盘了,在这个界面,没有意外的话应该可以看到我们刚刚分出来的硬盘分区:

选好硬盘点击安装,接下来进入漫长的等待:

!!!请注意!!!几分钟动一次鼠标,不要让电脑进入睡眠,因为可能因为驱动原因会睡死。 然后系统在安装过程需要一次重启,别担心,如果你在之前已经设置好优盘第一启动了的话,就不用管它,没有的话,需要手动选择优盘引导,进入优盘不用做什么,会继续安装:

跑完基本就可以进入系统的基础设置页面了:

选择什么不用说了吧,选择其他国家也可以,只要看得懂就好…

完成安装

完成安装可以进入桌面了,左上角有个 -> 关于本机

哎哟卧槽,这个显存不对劲啊….. 那现在我们暂时忍一下,使用憋屈的动画效果使用一下 Safari,去下载一个软件:Clover Configurator 安装软件

LaunchPad 打开运行,不出意外的话:

LaunchPad/其他/终端 运行以下命令,输入设置的用户密码就可以了:

1
sudo spctl --master-disable

重新打开 Clover Configurator 来到这个菜单:

我们现在的引导还是优盘,所以先在优盘那一项点击后面的挂载,这时候应该需要输入用户的密码,挂载成功完以后,就可以在 访达 看到 EFI 分区了。

还记得这张图吗:

将原来的

config.plist 备份一份,把这里面的适合自己机型的配置,改名为 config.plist 放在上一层文件夹:

然后重启电脑,进入优盘引导,根据下面字样提示,选择 Boot macOS from 你设置的分区名 即可进入安装好的系统。如果配置兼容的话,重新查看关于本机:

显存正常了。 再尝试音乐,如果声卡也正常,那就完成安装了。

迁入硬盘引导

一切好了,总不能总是插着硬盘来开机吧,我们需要把硬盘的文件替换到磁盘里面去,还是使用 Clover Configurator

一个是硬盘的,一个是优盘的,同时挂载出来。 然后使用 访达 进行操作:

OK,bios 重新设置第一启动是硬盘,然后,如果需要进入 osx 则选择 osx 硬盘回车,需要 Windows 的话就选择 Windows 所在的分区即可。

其他设置

这是我自己觉得比较好的设置,每个人可以按照自己的习惯来。 当然关于原来在 Windows 习惯的 Ctrl + c 这些都需要换成 Command + C 了,Command 在原来的 Win键 上面,我是不建议在系统设置里面替换这两者的按键位置的,毕竟用起来还蛮舒服的。 然后所有软件需要进入设置界面,都已经统一是 Command + , 这个快捷键了。

访达小设置

访达默认设置我感觉是不太好用的,比如点击访达,显示的不是我的电脑的内容,而是最近使用的内容,我特么写代码最近使用的内容都是代码文件,而我们通常不需要在这里去访问这些文件的(都是通过 IDE 工具),所以我希望点击访达进来的时候就是 我的个人文件夹: 打开 访达Command + ,

第二个不好的点是,我去到那个文件夹,要搜索某个文件,默认给我跑整个电脑去搜索,速度慢又没必要,所以:

第三个不好的点是,文件默认居然不排序,还要像垃圾堆一样堆在一起!!改掉设置!: 这个不在 Command + , 里边了,在这里:

开启Retina渲染

参考资料: 黑苹果开启缩放分辨率HiDPi以及字体模糊的调整方法总结 黑苹果开启HiDPI“解决”字体模糊的问题 直接在 其他软件/终端 粘贴下面的命令运行,提示输入用户密码,输入完以后根据提示来走就可以了:

1
sh -c "$(curl -fsSL https://raw.githubusercontent.com/xzhih/one-key-hidpi/master/hidpi.sh)"

重启电脑后安装 RDM 工具来配置分辨率(下载 RDM-2.2.dmg 就可以了 ) 运行,右上角:

选择带有小闪电的就是 HiDPI 的设置(也就是 Retina 效果)

一般根据屏幕原始分辨率来定,有 4K 显示器开 2K 分辨率的话,效果就跟白苹果一样了,2倍的差距,我目前原始分辨率是 1920 * 1080,开图中的分辨率也就凑合凑合过吧

然后在 系统偏好设置 –> 通用 里面开启字体平滑(在最下面,默认也是开启的)

我对默认的平滑程度还是不太喜欢,要开最高的,终端运行以下命令,提示输入用户密码,输入完以后重启即可:

1
defaults -currentHost write -globalDomain AppleFontSmoothing -int 3

来看看效果图: 开启前: 开启后:

结束

后面有空再更更常用软件了。

LaunchPad/其他/终端 运行以下命令,输入设置的用户密码就可以了:

1
sudo spctl --master-disable

从大学就开始折腾黑苹果,迟迟没有做些记录,总觉得差点什么,所以这次趁我需要重装系统的机会,拍了些照片,结合一些文字来做一个教程吧。 阅读按照文章顺序来,不要跳着来,除非我注明让你可以跳过。

OSX系统

不做评价谁好谁差,但是作为 Web工程师 的话,在 osx 系统下的编程体验确实要比 Windows 下好很多,软件也特别干净,从来没有在你工作的时候,突然右下角来个弹窗给你看广告,或者直接把广告就弹在最顶层来影响你的工作。 jdk npm git Dart Flutter 这些程序,在 unix 系的 osx 下也从来没有报错。所以还是推荐使用的。

安装须知

台式机

在我看来台式机安装黑果是比较完美的,能够屏蔽独立显卡,没有 WiFi 的顾虑。而且较高的性能使得能够更加完美的体验 osx 系统。 但是还是有些限制你需要知道:

  1. Intel 芯支持比较完美,如果是 AMD 芯的话,虽然说现在有破解,但是我还是觉得不如自然免驱的好(无测试过,没什么发言权);
  2. 一般比较热门的独立显卡,在国内远景论坛都有驱动的教程,涉及一些代码的注入,我比较懒,所以公司的电脑就拆掉了独显,使用集显来驱动,集显也是免驱的;

笔记本

  1. 笔记本受限比较大,现在笔记本一般都是 集显+独显 来自动切换达到省电的功能的,但是白苹果的笔记本,不是一个独显就是一个集显。所以,两者混合的情况下一般只能驱动集显(除非部分笔记本可以屏蔽集显),显卡问题就是独显基本就废了;
  2. WiFi 常年无解,两个方法解决:
    1. 淘宝购买 usb 接口的免驱网卡,卖家通常会提供使用所需要的驱动和软件;
    2. 更换笔记本内部的网卡,这一步比较专业,加上联想笔记本更换配件是需要刷白名单的,所以通常来说,都会选择第一种方式解决。

安装流程

镜像下载

因为在这篇文章产生的时候最新的 osx 系统是 10.15 catalina。所以我就是用这个系统来做教程吧。 osx_10.15_catalina 下载地址:下载地址

感谢远景论坛大佬的封装,我没有直接放大佬的最终下载地址,因为我还是希望各位能够给去下载,大佬有硬币赚。

镜像已经下好了,大佬也提供了常用的机型配置:

这一步只是先让你们看,先放一边,我们通常安装的时候不会直接选择合适的驱动,而是先使用通用驱动来安装,他是以最小的通用兼容配置来做的,通常都能够进入安装并且顺利通过。

写入优盘

此时,你需要拥有一个 16G 的优盘!记住记住,一定是 16G 不然他镜像可以刻录但是不会帮你刻录完整,不完整的镜像就是一个无法安装的镜像。(我用了一个 8G 的在这一步卡了好几个小时,最后把公司的师弟杀了祭天了,他出现了警告也不看直接点击 OK 就给我刻录了一个废盘) 首先你需要一个软件:TransMac 当然因为我的习惯,所以我都是在 Windows 系统下写优盘的。 安装,然后右键 -> 以管理员身份运行

会有提示需要购买正版什么的,不过点击 run 可以直接运行试用版,试用版就够用了。 然后找到你的优盘,右键选择 Format Disk –> Format with Disk Image 即可。

漫长的等待,结束了就可以使用这个优盘来安装系统了。

修改BIOS引导

msruefi,只能选择后者。一般新买的电脑或者近几年的电脑都是这个。 简单的识别方法:开机的时候,如果是品牌 Logo 然后就开始转圈圈进入 Windows 系统的话,就是 uefi。 第二就是修改 biossecurity boot,直接关闭就完事了。如果同等级的还有说什么引导什么系统的话,记得把它修改成 other

如果需要双系统

如果不需要双系统安装,直接跳过这一段

双系统指的是 Windows + osx,好像一般初学者都需要这个东西(包括之前的我自己)? 首先首先,Windows 默认的引导分区太小了,如果你不重新安装 Windows 的话,直接安装 osx 后面都会导致任何软件都无法安装。 教程 类似于上面地址里面的教程,但是在创建 efi 的时候,大小给大点,我一般给的是 512m,因为后面我们可能还需要根据不同的机型,在这个分区里面放入驱动。 分好区,先把 Windows 装上,记得留一些硬盘给 osx,我建议是 100G

装好系统后,在硬盘管理那里给 efi 分区随便给个盘符,然后把里面的东西备份一份出来,因为待会需要对他开刀,备份完整不至于你总是需要重新安装系统。

优盘引导开机

为了安装方便我建议将 first boot 设置为上面的优盘。当然使用 F12 来选择也行不过中途需要重启的时候就要留意重新选择了。 选择优盘中的带有 UEFI 选项的,这时候电脑将会读取优盘的引导,进入引导界面。

进入优盘是这个样子的:

一般是在第一项,下面会提示 Boot macOS Install from xxx 后面一般是优盘它默认给的名字。此时,我们 需要按一次 空格键 ,来到下一个页面:

下面是启动选项,一般按下空格需要设定的,大佬已经帮我们写好了,这个时候直接 按下回车键 进入安装软件就可以了。 一串命令后,我们会进来到这个页面:

能够来到这里,说明安装过程十分顺利,接下来进入下一个环节。

硬盘分区

就跟上面一样,我们选择磁盘工具,然后点击 继续,就会进入到磁盘工具的页面,那我们先选择显示所有设备,这时候磁盘都会显示出来。

然后我们选择整个硬盘(如果是单系统的话,双系统的话,你需要选择你要格式化的那个分区)点击上面的抹掉(看我鼠标那个位置):

点击之后会出来这个硬盘设定界面:

  1. 名称,可以自定义;

  2. 格式,新出来的文件系统是 APFS 听说是提速以及增加安全性的,那我就用这个好了,也建议使用这个。强烈不要使用区分大小写的分区格式(敲黑板),因为我使用了大小写区分的硬盘导致有些软件它不支持!!我目前就是因为这个原因重新安装系统的;

  3. 方案选择 GUID 分区图

稍稍等待一番,成功以后,直接左上角关闭磁盘管理工具。我们就可以安装系统了。

安装系统

回到这个界面,选择 安装 macOS


然后就同意协议了,这个不用图都知道要选择什么吧… 同意协议之后就是选择硬盘了,在这个界面,没有意外的话应该可以看到我们刚刚分出来的硬盘分区:

选好硬盘点击安装,接下来进入漫长的等待:

!!!请注意!!!几分钟动一次鼠标,不要让电脑进入睡眠,因为可能因为驱动原因会睡死。 然后系统在安装过程需要一次重启,别担心,如果你在之前已经设置好优盘第一启动了的话,就不用管它,没有的话,需要手动选择优盘引导,进入优盘不用做什么,会继续安装:

跑完基本就可以进入系统的基础设置页面了:

选择什么不用说了吧,选择其他国家也可以,只要看得懂就好…

完成安装

完成安装可以进入桌面了,左上角有个 -> 关于本机

哎哟卧槽,这个显存不对劲啊….. 那现在我们暂时忍一下,使用憋屈的动画效果使用一下 Safari,去下载一个软件:Clover Configurator 安装软件

LaunchPad 打开运行,不出意外的话:

LaunchPad/其他/终端 运行以下命令,输入设置的用户密码就可以了:

1
sudo spctl --master-disable

重新打开 Clover Configurator 来到这个菜单:

我们现在的引导还是优盘,所以先在优盘那一项点击后面的挂载,这时候应该需要输入用户的密码,挂载成功完以后,就可以在 访达 看到 EFI 分区了。

还记得这张图吗:

将原来的

config.plist 备份一份,把这里面的适合自己机型的配置,改名为 config.plist 放在上一层文件夹:

然后重启电脑,进入优盘引导,根据下面字样提示,选择 Boot macOS from 你设置的分区名 即可进入安装好的系统。如果配置兼容的话,重新查看关于本机:

显存正常了。 再尝试音乐,如果声卡也正常,那就完成安装了。

迁入硬盘引导

一切好了,总不能总是插着硬盘来开机吧,我们需要把硬盘的文件替换到磁盘里面去,还是使用 Clover Configurator

一个是硬盘的,一个是优盘的,同时挂载出来。 然后使用 访达 进行操作:

OK,bios 重新设置第一启动是硬盘,然后,如果需要进入 osx 则选择 osx 硬盘回车,需要 Windows 的话就选择 Windows 所在的分区即可。

其他设置

这是我自己觉得比较好的设置,每个人可以按照自己的习惯来。 当然关于原来在 Windows 习惯的 Ctrl + c 这些都需要换成 Command + C 了,Command 在原来的 Win键 上面,我是不建议在系统设置里面替换这两者的按键位置的,毕竟用起来还蛮舒服的。 然后所有软件需要进入设置界面,都已经统一是 Command + , 这个快捷键了。

访达小设置

访达默认设置我感觉是不太好用的,比如点击访达,显示的不是我的电脑的内容,而是最近使用的内容,我特么写代码最近使用的内容都是代码文件,而我们通常不需要在这里去访问这些文件的(都是通过 IDE 工具),所以我希望点击访达进来的时候就是 我的个人文件夹: 打开 访达Command + ,

第二个不好的点是,我去到那个文件夹,要搜索某个文件,默认给我跑整个电脑去搜索,速度慢又没必要,所以:

第三个不好的点是,文件默认居然不排序,还要像垃圾堆一样堆在一起!!改掉设置!: 这个不在 Command + , 里边了,在这里:

开启Retina渲染

参考资料: 黑苹果开启缩放分辨率HiDPi以及字体模糊的调整方法总结 黑苹果开启HiDPI“解决”字体模糊的问题 直接在 其他软件/终端 粘贴下面的命令运行,提示输入用户密码,输入完以后根据提示来走就可以了:

1
sh -c "$(curl -fsSL https://raw.githubusercontent.com/xzhih/one-key-hidpi/master/hidpi.sh)"

重启电脑后安装 RDM 工具来配置分辨率(下载 RDM-2.2.dmg 就可以了 ) 运行,右上角:

选择带有小闪电的就是 HiDPI 的设置(也就是 Retina 效果)

一般根据屏幕原始分辨率来定,有 4K 显示器开 2K 分辨率的话,效果就跟白苹果一样了,2倍的差距,我目前原始分辨率是 1920 * 1080,开图中的分辨率也就凑合凑合过吧

然后在 系统偏好设置 –> 通用 里面开启字体平滑(在最下面,默认也是开启的)

我对默认的平滑程度还是不太喜欢,要开最高的,终端运行以下命令,提示输入用户密码,输入完以后重启即可:

1
defaults -currentHost write -globalDomain AppleFontSmoothing -int 3

来看看效果图: 开启前: 开启后:

结束

后面有空再更更常用软件了。

重新打开 Clover Configurator 来到这个菜单:

从大学就开始折腾黑苹果,迟迟没有做些记录,总觉得差点什么,所以这次趁我需要重装系统的机会,拍了些照片,结合一些文字来做一个教程吧。 阅读按照文章顺序来,不要跳着来,除非我注明让你可以跳过。

OSX系统

不做评价谁好谁差,但是作为 Web工程师 的话,在 osx 系统下的编程体验确实要比 Windows 下好很多,软件也特别干净,从来没有在你工作的时候,突然右下角来个弹窗给你看广告,或者直接把广告就弹在最顶层来影响你的工作。 jdk npm git Dart Flutter 这些程序,在 unix 系的 osx 下也从来没有报错。所以还是推荐使用的。

安装须知

台式机

在我看来台式机安装黑果是比较完美的,能够屏蔽独立显卡,没有 WiFi 的顾虑。而且较高的性能使得能够更加完美的体验 osx 系统。 但是还是有些限制你需要知道:

  1. Intel 芯支持比较完美,如果是 AMD 芯的话,虽然说现在有破解,但是我还是觉得不如自然免驱的好(无测试过,没什么发言权);
  2. 一般比较热门的独立显卡,在国内远景论坛都有驱动的教程,涉及一些代码的注入,我比较懒,所以公司的电脑就拆掉了独显,使用集显来驱动,集显也是免驱的;

笔记本

  1. 笔记本受限比较大,现在笔记本一般都是 集显+独显 来自动切换达到省电的功能的,但是白苹果的笔记本,不是一个独显就是一个集显。所以,两者混合的情况下一般只能驱动集显(除非部分笔记本可以屏蔽集显),显卡问题就是独显基本就废了;
  2. WiFi 常年无解,两个方法解决:
    1. 淘宝购买 usb 接口的免驱网卡,卖家通常会提供使用所需要的驱动和软件;
    2. 更换笔记本内部的网卡,这一步比较专业,加上联想笔记本更换配件是需要刷白名单的,所以通常来说,都会选择第一种方式解决。

安装流程

镜像下载

因为在这篇文章产生的时候最新的 osx 系统是 10.15 catalina。所以我就是用这个系统来做教程吧。 osx_10.15_catalina 下载地址:下载地址

感谢远景论坛大佬的封装,我没有直接放大佬的最终下载地址,因为我还是希望各位能够给去下载,大佬有硬币赚。

镜像已经下好了,大佬也提供了常用的机型配置:

这一步只是先让你们看,先放一边,我们通常安装的时候不会直接选择合适的驱动,而是先使用通用驱动来安装,他是以最小的通用兼容配置来做的,通常都能够进入安装并且顺利通过。

写入优盘

此时,你需要拥有一个 16G 的优盘!记住记住,一定是 16G 不然他镜像可以刻录但是不会帮你刻录完整,不完整的镜像就是一个无法安装的镜像。(我用了一个 8G 的在这一步卡了好几个小时,最后把公司的师弟杀了祭天了,他出现了警告也不看直接点击 OK 就给我刻录了一个废盘) 首先你需要一个软件:TransMac 当然因为我的习惯,所以我都是在 Windows 系统下写优盘的。 安装,然后右键 -> 以管理员身份运行

会有提示需要购买正版什么的,不过点击 run 可以直接运行试用版,试用版就够用了。 然后找到你的优盘,右键选择 Format Disk –> Format with Disk Image 即可。

漫长的等待,结束了就可以使用这个优盘来安装系统了。

修改BIOS引导

msruefi,只能选择后者。一般新买的电脑或者近几年的电脑都是这个。 简单的识别方法:开机的时候,如果是品牌 Logo 然后就开始转圈圈进入 Windows 系统的话,就是 uefi。 第二就是修改 biossecurity boot,直接关闭就完事了。如果同等级的还有说什么引导什么系统的话,记得把它修改成 other

如果需要双系统

如果不需要双系统安装,直接跳过这一段

双系统指的是 Windows + osx,好像一般初学者都需要这个东西(包括之前的我自己)? 首先首先,Windows 默认的引导分区太小了,如果你不重新安装 Windows 的话,直接安装 osx 后面都会导致任何软件都无法安装。 教程 类似于上面地址里面的教程,但是在创建 efi 的时候,大小给大点,我一般给的是 512m,因为后面我们可能还需要根据不同的机型,在这个分区里面放入驱动。 分好区,先把 Windows 装上,记得留一些硬盘给 osx,我建议是 100G

装好系统后,在硬盘管理那里给 efi 分区随便给个盘符,然后把里面的东西备份一份出来,因为待会需要对他开刀,备份完整不至于你总是需要重新安装系统。

优盘引导开机

为了安装方便我建议将 first boot 设置为上面的优盘。当然使用 F12 来选择也行不过中途需要重启的时候就要留意重新选择了。 选择优盘中的带有 UEFI 选项的,这时候电脑将会读取优盘的引导,进入引导界面。

进入优盘是这个样子的:

一般是在第一项,下面会提示 Boot macOS Install from xxx 后面一般是优盘它默认给的名字。此时,我们 需要按一次 空格键 ,来到下一个页面:

下面是启动选项,一般按下空格需要设定的,大佬已经帮我们写好了,这个时候直接 按下回车键 进入安装软件就可以了。 一串命令后,我们会进来到这个页面:

能够来到这里,说明安装过程十分顺利,接下来进入下一个环节。

硬盘分区

就跟上面一样,我们选择磁盘工具,然后点击 继续,就会进入到磁盘工具的页面,那我们先选择显示所有设备,这时候磁盘都会显示出来。

然后我们选择整个硬盘(如果是单系统的话,双系统的话,你需要选择你要格式化的那个分区)点击上面的抹掉(看我鼠标那个位置):

点击之后会出来这个硬盘设定界面:

  1. 名称,可以自定义;

  2. 格式,新出来的文件系统是 APFS 听说是提速以及增加安全性的,那我就用这个好了,也建议使用这个。强烈不要使用区分大小写的分区格式(敲黑板),因为我使用了大小写区分的硬盘导致有些软件它不支持!!我目前就是因为这个原因重新安装系统的;

  3. 方案选择 GUID 分区图

稍稍等待一番,成功以后,直接左上角关闭磁盘管理工具。我们就可以安装系统了。

安装系统

回到这个界面,选择 安装 macOS


然后就同意协议了,这个不用图都知道要选择什么吧… 同意协议之后就是选择硬盘了,在这个界面,没有意外的话应该可以看到我们刚刚分出来的硬盘分区:

选好硬盘点击安装,接下来进入漫长的等待:

!!!请注意!!!几分钟动一次鼠标,不要让电脑进入睡眠,因为可能因为驱动原因会睡死。 然后系统在安装过程需要一次重启,别担心,如果你在之前已经设置好优盘第一启动了的话,就不用管它,没有的话,需要手动选择优盘引导,进入优盘不用做什么,会继续安装:

跑完基本就可以进入系统的基础设置页面了:

选择什么不用说了吧,选择其他国家也可以,只要看得懂就好…

完成安装

完成安装可以进入桌面了,左上角有个 -> 关于本机

哎哟卧槽,这个显存不对劲啊….. 那现在我们暂时忍一下,使用憋屈的动画效果使用一下 Safari,去下载一个软件:Clover Configurator 安装软件

LaunchPad 打开运行,不出意外的话:

LaunchPad/其他/终端 运行以下命令,输入设置的用户密码就可以了:

1
sudo spctl --master-disable

重新打开 Clover Configurator 来到这个菜单:

我们现在的引导还是优盘,所以先在优盘那一项点击后面的挂载,这时候应该需要输入用户的密码,挂载成功完以后,就可以在 访达 看到 EFI 分区了。

还记得这张图吗:

将原来的

config.plist 备份一份,把这里面的适合自己机型的配置,改名为 config.plist 放在上一层文件夹:

然后重启电脑,进入优盘引导,根据下面字样提示,选择 Boot macOS from 你设置的分区名 即可进入安装好的系统。如果配置兼容的话,重新查看关于本机:

显存正常了。 再尝试音乐,如果声卡也正常,那就完成安装了。

迁入硬盘引导

一切好了,总不能总是插着硬盘来开机吧,我们需要把硬盘的文件替换到磁盘里面去,还是使用 Clover Configurator

一个是硬盘的,一个是优盘的,同时挂载出来。 然后使用 访达 进行操作:

OK,bios 重新设置第一启动是硬盘,然后,如果需要进入 osx 则选择 osx 硬盘回车,需要 Windows 的话就选择 Windows 所在的分区即可。

其他设置

这是我自己觉得比较好的设置,每个人可以按照自己的习惯来。 当然关于原来在 Windows 习惯的 Ctrl + c 这些都需要换成 Command + C 了,Command 在原来的 Win键 上面,我是不建议在系统设置里面替换这两者的按键位置的,毕竟用起来还蛮舒服的。 然后所有软件需要进入设置界面,都已经统一是 Command + , 这个快捷键了。

访达小设置

访达默认设置我感觉是不太好用的,比如点击访达,显示的不是我的电脑的内容,而是最近使用的内容,我特么写代码最近使用的内容都是代码文件,而我们通常不需要在这里去访问这些文件的(都是通过 IDE 工具),所以我希望点击访达进来的时候就是 我的个人文件夹: 打开 访达Command + ,

第二个不好的点是,我去到那个文件夹,要搜索某个文件,默认给我跑整个电脑去搜索,速度慢又没必要,所以:

第三个不好的点是,文件默认居然不排序,还要像垃圾堆一样堆在一起!!改掉设置!: 这个不在 Command + , 里边了,在这里:

开启Retina渲染

参考资料: 黑苹果开启缩放分辨率HiDPi以及字体模糊的调整方法总结 黑苹果开启HiDPI“解决”字体模糊的问题 直接在 其他软件/终端 粘贴下面的命令运行,提示输入用户密码,输入完以后根据提示来走就可以了:

1
sh -c "$(curl -fsSL https://raw.githubusercontent.com/xzhih/one-key-hidpi/master/hidpi.sh)"

重启电脑后安装 RDM 工具来配置分辨率(下载 RDM-2.2.dmg 就可以了 ) 运行,右上角:

选择带有小闪电的就是 HiDPI 的设置(也就是 Retina 效果)

一般根据屏幕原始分辨率来定,有 4K 显示器开 2K 分辨率的话,效果就跟白苹果一样了,2倍的差距,我目前原始分辨率是 1920 * 1080,开图中的分辨率也就凑合凑合过吧

然后在 系统偏好设置 –> 通用 里面开启字体平滑(在最下面,默认也是开启的)

我对默认的平滑程度还是不太喜欢,要开最高的,终端运行以下命令,提示输入用户密码,输入完以后重启即可:

1
defaults -currentHost write -globalDomain AppleFontSmoothing -int 3

来看看效果图: 开启前: 开启后:

结束

后面有空再更更常用软件了。

我们现在的引导还是优盘,所以先在优盘那一项点击后面的挂载,这时候应该需要输入用户的密码,挂载成功完以后,就可以在 访达 看到 EFI 分区了。

从大学就开始折腾黑苹果,迟迟没有做些记录,总觉得差点什么,所以这次趁我需要重装系统的机会,拍了些照片,结合一些文字来做一个教程吧。 阅读按照文章顺序来,不要跳着来,除非我注明让你可以跳过。

OSX系统

不做评价谁好谁差,但是作为 Web工程师 的话,在 osx 系统下的编程体验确实要比 Windows 下好很多,软件也特别干净,从来没有在你工作的时候,突然右下角来个弹窗给你看广告,或者直接把广告就弹在最顶层来影响你的工作。 jdk npm git Dart Flutter 这些程序,在 unix 系的 osx 下也从来没有报错。所以还是推荐使用的。

安装须知

台式机

在我看来台式机安装黑果是比较完美的,能够屏蔽独立显卡,没有 WiFi 的顾虑。而且较高的性能使得能够更加完美的体验 osx 系统。 但是还是有些限制你需要知道:

  1. Intel 芯支持比较完美,如果是 AMD 芯的话,虽然说现在有破解,但是我还是觉得不如自然免驱的好(无测试过,没什么发言权);
  2. 一般比较热门的独立显卡,在国内远景论坛都有驱动的教程,涉及一些代码的注入,我比较懒,所以公司的电脑就拆掉了独显,使用集显来驱动,集显也是免驱的;

笔记本

  1. 笔记本受限比较大,现在笔记本一般都是 集显+独显 来自动切换达到省电的功能的,但是白苹果的笔记本,不是一个独显就是一个集显。所以,两者混合的情况下一般只能驱动集显(除非部分笔记本可以屏蔽集显),显卡问题就是独显基本就废了;
  2. WiFi 常年无解,两个方法解决:
    1. 淘宝购买 usb 接口的免驱网卡,卖家通常会提供使用所需要的驱动和软件;
    2. 更换笔记本内部的网卡,这一步比较专业,加上联想笔记本更换配件是需要刷白名单的,所以通常来说,都会选择第一种方式解决。

安装流程

镜像下载

因为在这篇文章产生的时候最新的 osx 系统是 10.15 catalina。所以我就是用这个系统来做教程吧。 osx_10.15_catalina 下载地址:下载地址

感谢远景论坛大佬的封装,我没有直接放大佬的最终下载地址,因为我还是希望各位能够给去下载,大佬有硬币赚。

镜像已经下好了,大佬也提供了常用的机型配置:

这一步只是先让你们看,先放一边,我们通常安装的时候不会直接选择合适的驱动,而是先使用通用驱动来安装,他是以最小的通用兼容配置来做的,通常都能够进入安装并且顺利通过。

写入优盘

此时,你需要拥有一个 16G 的优盘!记住记住,一定是 16G 不然他镜像可以刻录但是不会帮你刻录完整,不完整的镜像就是一个无法安装的镜像。(我用了一个 8G 的在这一步卡了好几个小时,最后把公司的师弟杀了祭天了,他出现了警告也不看直接点击 OK 就给我刻录了一个废盘) 首先你需要一个软件:TransMac 当然因为我的习惯,所以我都是在 Windows 系统下写优盘的。 安装,然后右键 -> 以管理员身份运行

会有提示需要购买正版什么的,不过点击 run 可以直接运行试用版,试用版就够用了。 然后找到你的优盘,右键选择 Format Disk –> Format with Disk Image 即可。

漫长的等待,结束了就可以使用这个优盘来安装系统了。

修改BIOS引导

msruefi,只能选择后者。一般新买的电脑或者近几年的电脑都是这个。 简单的识别方法:开机的时候,如果是品牌 Logo 然后就开始转圈圈进入 Windows 系统的话,就是 uefi。 第二就是修改 biossecurity boot,直接关闭就完事了。如果同等级的还有说什么引导什么系统的话,记得把它修改成 other

如果需要双系统

如果不需要双系统安装,直接跳过这一段

双系统指的是 Windows + osx,好像一般初学者都需要这个东西(包括之前的我自己)? 首先首先,Windows 默认的引导分区太小了,如果你不重新安装 Windows 的话,直接安装 osx 后面都会导致任何软件都无法安装。 教程 类似于上面地址里面的教程,但是在创建 efi 的时候,大小给大点,我一般给的是 512m,因为后面我们可能还需要根据不同的机型,在这个分区里面放入驱动。 分好区,先把 Windows 装上,记得留一些硬盘给 osx,我建议是 100G

装好系统后,在硬盘管理那里给 efi 分区随便给个盘符,然后把里面的东西备份一份出来,因为待会需要对他开刀,备份完整不至于你总是需要重新安装系统。

优盘引导开机

为了安装方便我建议将 first boot 设置为上面的优盘。当然使用 F12 来选择也行不过中途需要重启的时候就要留意重新选择了。 选择优盘中的带有 UEFI 选项的,这时候电脑将会读取优盘的引导,进入引导界面。

进入优盘是这个样子的:

一般是在第一项,下面会提示 Boot macOS Install from xxx 后面一般是优盘它默认给的名字。此时,我们 需要按一次 空格键 ,来到下一个页面:

下面是启动选项,一般按下空格需要设定的,大佬已经帮我们写好了,这个时候直接 按下回车键 进入安装软件就可以了。 一串命令后,我们会进来到这个页面:

能够来到这里,说明安装过程十分顺利,接下来进入下一个环节。

硬盘分区

就跟上面一样,我们选择磁盘工具,然后点击 继续,就会进入到磁盘工具的页面,那我们先选择显示所有设备,这时候磁盘都会显示出来。

然后我们选择整个硬盘(如果是单系统的话,双系统的话,你需要选择你要格式化的那个分区)点击上面的抹掉(看我鼠标那个位置):

点击之后会出来这个硬盘设定界面:

  1. 名称,可以自定义;

  2. 格式,新出来的文件系统是 APFS 听说是提速以及增加安全性的,那我就用这个好了,也建议使用这个。强烈不要使用区分大小写的分区格式(敲黑板),因为我使用了大小写区分的硬盘导致有些软件它不支持!!我目前就是因为这个原因重新安装系统的;

  3. 方案选择 GUID 分区图

稍稍等待一番,成功以后,直接左上角关闭磁盘管理工具。我们就可以安装系统了。

安装系统

回到这个界面,选择 安装 macOS


然后就同意协议了,这个不用图都知道要选择什么吧… 同意协议之后就是选择硬盘了,在这个界面,没有意外的话应该可以看到我们刚刚分出来的硬盘分区:

选好硬盘点击安装,接下来进入漫长的等待:

!!!请注意!!!几分钟动一次鼠标,不要让电脑进入睡眠,因为可能因为驱动原因会睡死。 然后系统在安装过程需要一次重启,别担心,如果你在之前已经设置好优盘第一启动了的话,就不用管它,没有的话,需要手动选择优盘引导,进入优盘不用做什么,会继续安装:

跑完基本就可以进入系统的基础设置页面了:

选择什么不用说了吧,选择其他国家也可以,只要看得懂就好…

完成安装

完成安装可以进入桌面了,左上角有个 -> 关于本机

哎哟卧槽,这个显存不对劲啊….. 那现在我们暂时忍一下,使用憋屈的动画效果使用一下 Safari,去下载一个软件:Clover Configurator 安装软件

LaunchPad 打开运行,不出意外的话:

LaunchPad/其他/终端 运行以下命令,输入设置的用户密码就可以了:

1
sudo spctl --master-disable

重新打开 Clover Configurator 来到这个菜单:

我们现在的引导还是优盘,所以先在优盘那一项点击后面的挂载,这时候应该需要输入用户的密码,挂载成功完以后,就可以在 访达 看到 EFI 分区了。

还记得这张图吗:

将原来的

config.plist 备份一份,把这里面的适合自己机型的配置,改名为 config.plist 放在上一层文件夹:

然后重启电脑,进入优盘引导,根据下面字样提示,选择 Boot macOS from 你设置的分区名 即可进入安装好的系统。如果配置兼容的话,重新查看关于本机:

显存正常了。 再尝试音乐,如果声卡也正常,那就完成安装了。

迁入硬盘引导

一切好了,总不能总是插着硬盘来开机吧,我们需要把硬盘的文件替换到磁盘里面去,还是使用 Clover Configurator

一个是硬盘的,一个是优盘的,同时挂载出来。 然后使用 访达 进行操作:

OK,bios 重新设置第一启动是硬盘,然后,如果需要进入 osx 则选择 osx 硬盘回车,需要 Windows 的话就选择 Windows 所在的分区即可。

其他设置

这是我自己觉得比较好的设置,每个人可以按照自己的习惯来。 当然关于原来在 Windows 习惯的 Ctrl + c 这些都需要换成 Command + C 了,Command 在原来的 Win键 上面,我是不建议在系统设置里面替换这两者的按键位置的,毕竟用起来还蛮舒服的。 然后所有软件需要进入设置界面,都已经统一是 Command + , 这个快捷键了。

访达小设置

访达默认设置我感觉是不太好用的,比如点击访达,显示的不是我的电脑的内容,而是最近使用的内容,我特么写代码最近使用的内容都是代码文件,而我们通常不需要在这里去访问这些文件的(都是通过 IDE 工具),所以我希望点击访达进来的时候就是 我的个人文件夹: 打开 访达Command + ,

第二个不好的点是,我去到那个文件夹,要搜索某个文件,默认给我跑整个电脑去搜索,速度慢又没必要,所以:

第三个不好的点是,文件默认居然不排序,还要像垃圾堆一样堆在一起!!改掉设置!: 这个不在 Command + , 里边了,在这里:

开启Retina渲染

参考资料: 黑苹果开启缩放分辨率HiDPi以及字体模糊的调整方法总结 黑苹果开启HiDPI“解决”字体模糊的问题 直接在 其他软件/终端 粘贴下面的命令运行,提示输入用户密码,输入完以后根据提示来走就可以了:

1
sh -c "$(curl -fsSL https://raw.githubusercontent.com/xzhih/one-key-hidpi/master/hidpi.sh)"

重启电脑后安装 RDM 工具来配置分辨率(下载 RDM-2.2.dmg 就可以了 ) 运行,右上角:

选择带有小闪电的就是 HiDPI 的设置(也就是 Retina 效果)

一般根据屏幕原始分辨率来定,有 4K 显示器开 2K 分辨率的话,效果就跟白苹果一样了,2倍的差距,我目前原始分辨率是 1920 * 1080,开图中的分辨率也就凑合凑合过吧

然后在 系统偏好设置 –> 通用 里面开启字体平滑(在最下面,默认也是开启的)

我对默认的平滑程度还是不太喜欢,要开最高的,终端运行以下命令,提示输入用户密码,输入完以后重启即可:

1
defaults -currentHost write -globalDomain AppleFontSmoothing -int 3

来看看效果图: 开启前: 开启后:

结束

后面有空再更更常用软件了。

还记得这张图吗:

从大学就开始折腾黑苹果,迟迟没有做些记录,总觉得差点什么,所以这次趁我需要重装系统的机会,拍了些照片,结合一些文字来做一个教程吧。 阅读按照文章顺序来,不要跳着来,除非我注明让你可以跳过。

OSX系统

不做评价谁好谁差,但是作为 Web工程师 的话,在 osx 系统下的编程体验确实要比 Windows 下好很多,软件也特别干净,从来没有在你工作的时候,突然右下角来个弹窗给你看广告,或者直接把广告就弹在最顶层来影响你的工作。 jdk npm git Dart Flutter 这些程序,在 unix 系的 osx 下也从来没有报错。所以还是推荐使用的。

安装须知

台式机

在我看来台式机安装黑果是比较完美的,能够屏蔽独立显卡,没有 WiFi 的顾虑。而且较高的性能使得能够更加完美的体验 osx 系统。 但是还是有些限制你需要知道:

  1. Intel 芯支持比较完美,如果是 AMD 芯的话,虽然说现在有破解,但是我还是觉得不如自然免驱的好(无测试过,没什么发言权);
  2. 一般比较热门的独立显卡,在国内远景论坛都有驱动的教程,涉及一些代码的注入,我比较懒,所以公司的电脑就拆掉了独显,使用集显来驱动,集显也是免驱的;

笔记本

  1. 笔记本受限比较大,现在笔记本一般都是 集显+独显 来自动切换达到省电的功能的,但是白苹果的笔记本,不是一个独显就是一个集显。所以,两者混合的情况下一般只能驱动集显(除非部分笔记本可以屏蔽集显),显卡问题就是独显基本就废了;
  2. WiFi 常年无解,两个方法解决:
    1. 淘宝购买 usb 接口的免驱网卡,卖家通常会提供使用所需要的驱动和软件;
    2. 更换笔记本内部的网卡,这一步比较专业,加上联想笔记本更换配件是需要刷白名单的,所以通常来说,都会选择第一种方式解决。

安装流程

镜像下载

因为在这篇文章产生的时候最新的 osx 系统是 10.15 catalina。所以我就是用这个系统来做教程吧。 osx_10.15_catalina 下载地址:下载地址

感谢远景论坛大佬的封装,我没有直接放大佬的最终下载地址,因为我还是希望各位能够给去下载,大佬有硬币赚。

镜像已经下好了,大佬也提供了常用的机型配置:

这一步只是先让你们看,先放一边,我们通常安装的时候不会直接选择合适的驱动,而是先使用通用驱动来安装,他是以最小的通用兼容配置来做的,通常都能够进入安装并且顺利通过。

写入优盘

此时,你需要拥有一个 16G 的优盘!记住记住,一定是 16G 不然他镜像可以刻录但是不会帮你刻录完整,不完整的镜像就是一个无法安装的镜像。(我用了一个 8G 的在这一步卡了好几个小时,最后把公司的师弟杀了祭天了,他出现了警告也不看直接点击 OK 就给我刻录了一个废盘) 首先你需要一个软件:TransMac 当然因为我的习惯,所以我都是在 Windows 系统下写优盘的。 安装,然后右键 -> 以管理员身份运行

会有提示需要购买正版什么的,不过点击 run 可以直接运行试用版,试用版就够用了。 然后找到你的优盘,右键选择 Format Disk –> Format with Disk Image 即可。

漫长的等待,结束了就可以使用这个优盘来安装系统了。

修改BIOS引导

msruefi,只能选择后者。一般新买的电脑或者近几年的电脑都是这个。 简单的识别方法:开机的时候,如果是品牌 Logo 然后就开始转圈圈进入 Windows 系统的话,就是 uefi。 第二就是修改 biossecurity boot,直接关闭就完事了。如果同等级的还有说什么引导什么系统的话,记得把它修改成 other

如果需要双系统

如果不需要双系统安装,直接跳过这一段

双系统指的是 Windows + osx,好像一般初学者都需要这个东西(包括之前的我自己)? 首先首先,Windows 默认的引导分区太小了,如果你不重新安装 Windows 的话,直接安装 osx 后面都会导致任何软件都无法安装。 教程 类似于上面地址里面的教程,但是在创建 efi 的时候,大小给大点,我一般给的是 512m,因为后面我们可能还需要根据不同的机型,在这个分区里面放入驱动。 分好区,先把 Windows 装上,记得留一些硬盘给 osx,我建议是 100G

装好系统后,在硬盘管理那里给 efi 分区随便给个盘符,然后把里面的东西备份一份出来,因为待会需要对他开刀,备份完整不至于你总是需要重新安装系统。

优盘引导开机

为了安装方便我建议将 first boot 设置为上面的优盘。当然使用 F12 来选择也行不过中途需要重启的时候就要留意重新选择了。 选择优盘中的带有 UEFI 选项的,这时候电脑将会读取优盘的引导,进入引导界面。

进入优盘是这个样子的:

一般是在第一项,下面会提示 Boot macOS Install from xxx 后面一般是优盘它默认给的名字。此时,我们 需要按一次 空格键 ,来到下一个页面:

下面是启动选项,一般按下空格需要设定的,大佬已经帮我们写好了,这个时候直接 按下回车键 进入安装软件就可以了。 一串命令后,我们会进来到这个页面:

能够来到这里,说明安装过程十分顺利,接下来进入下一个环节。

硬盘分区

就跟上面一样,我们选择磁盘工具,然后点击 继续,就会进入到磁盘工具的页面,那我们先选择显示所有设备,这时候磁盘都会显示出来。

然后我们选择整个硬盘(如果是单系统的话,双系统的话,你需要选择你要格式化的那个分区)点击上面的抹掉(看我鼠标那个位置):

点击之后会出来这个硬盘设定界面:

  1. 名称,可以自定义;

  2. 格式,新出来的文件系统是 APFS 听说是提速以及增加安全性的,那我就用这个好了,也建议使用这个。强烈不要使用区分大小写的分区格式(敲黑板),因为我使用了大小写区分的硬盘导致有些软件它不支持!!我目前就是因为这个原因重新安装系统的;

  3. 方案选择 GUID 分区图

稍稍等待一番,成功以后,直接左上角关闭磁盘管理工具。我们就可以安装系统了。

安装系统

回到这个界面,选择 安装 macOS


然后就同意协议了,这个不用图都知道要选择什么吧… 同意协议之后就是选择硬盘了,在这个界面,没有意外的话应该可以看到我们刚刚分出来的硬盘分区:

选好硬盘点击安装,接下来进入漫长的等待:

!!!请注意!!!几分钟动一次鼠标,不要让电脑进入睡眠,因为可能因为驱动原因会睡死。 然后系统在安装过程需要一次重启,别担心,如果你在之前已经设置好优盘第一启动了的话,就不用管它,没有的话,需要手动选择优盘引导,进入优盘不用做什么,会继续安装:

跑完基本就可以进入系统的基础设置页面了:

选择什么不用说了吧,选择其他国家也可以,只要看得懂就好…

完成安装

完成安装可以进入桌面了,左上角有个 -> 关于本机

哎哟卧槽,这个显存不对劲啊….. 那现在我们暂时忍一下,使用憋屈的动画效果使用一下 Safari,去下载一个软件:Clover Configurator 安装软件

LaunchPad 打开运行,不出意外的话:

LaunchPad/其他/终端 运行以下命令,输入设置的用户密码就可以了:

1
sudo spctl --master-disable

重新打开 Clover Configurator 来到这个菜单:

我们现在的引导还是优盘,所以先在优盘那一项点击后面的挂载,这时候应该需要输入用户的密码,挂载成功完以后,就可以在 访达 看到 EFI 分区了。

还记得这张图吗:

将原来的

config.plist 备份一份,把这里面的适合自己机型的配置,改名为 config.plist 放在上一层文件夹:

然后重启电脑,进入优盘引导,根据下面字样提示,选择 Boot macOS from 你设置的分区名 即可进入安装好的系统。如果配置兼容的话,重新查看关于本机:

显存正常了。 再尝试音乐,如果声卡也正常,那就完成安装了。

迁入硬盘引导

一切好了,总不能总是插着硬盘来开机吧,我们需要把硬盘的文件替换到磁盘里面去,还是使用 Clover Configurator

一个是硬盘的,一个是优盘的,同时挂载出来。 然后使用 访达 进行操作:

OK,bios 重新设置第一启动是硬盘,然后,如果需要进入 osx 则选择 osx 硬盘回车,需要 Windows 的话就选择 Windows 所在的分区即可。

其他设置

这是我自己觉得比较好的设置,每个人可以按照自己的习惯来。 当然关于原来在 Windows 习惯的 Ctrl + c 这些都需要换成 Command + C 了,Command 在原来的 Win键 上面,我是不建议在系统设置里面替换这两者的按键位置的,毕竟用起来还蛮舒服的。 然后所有软件需要进入设置界面,都已经统一是 Command + , 这个快捷键了。

访达小设置

访达默认设置我感觉是不太好用的,比如点击访达,显示的不是我的电脑的内容,而是最近使用的内容,我特么写代码最近使用的内容都是代码文件,而我们通常不需要在这里去访问这些文件的(都是通过 IDE 工具),所以我希望点击访达进来的时候就是 我的个人文件夹: 打开 访达Command + ,

第二个不好的点是,我去到那个文件夹,要搜索某个文件,默认给我跑整个电脑去搜索,速度慢又没必要,所以:

第三个不好的点是,文件默认居然不排序,还要像垃圾堆一样堆在一起!!改掉设置!: 这个不在 Command + , 里边了,在这里:

开启Retina渲染

参考资料: 黑苹果开启缩放分辨率HiDPi以及字体模糊的调整方法总结 黑苹果开启HiDPI“解决”字体模糊的问题 直接在 其他软件/终端 粘贴下面的命令运行,提示输入用户密码,输入完以后根据提示来走就可以了:

1
sh -c "$(curl -fsSL https://raw.githubusercontent.com/xzhih/one-key-hidpi/master/hidpi.sh)"

重启电脑后安装 RDM 工具来配置分辨率(下载 RDM-2.2.dmg 就可以了 ) 运行,右上角:

选择带有小闪电的就是 HiDPI 的设置(也就是 Retina 效果)

一般根据屏幕原始分辨率来定,有 4K 显示器开 2K 分辨率的话,效果就跟白苹果一样了,2倍的差距,我目前原始分辨率是 1920 * 1080,开图中的分辨率也就凑合凑合过吧

然后在 系统偏好设置 –> 通用 里面开启字体平滑(在最下面,默认也是开启的)

我对默认的平滑程度还是不太喜欢,要开最高的,终端运行以下命令,提示输入用户密码,输入完以后重启即可:

1
defaults -currentHost write -globalDomain AppleFontSmoothing -int 3

来看看效果图: 开启前: 开启后:

结束

后面有空再更更常用软件了。 将原来的

config.plist 备份一份,把这里面的适合自己机型的配置,改名为 config.plist 放在上一层文件夹:

从大学就开始折腾黑苹果,迟迟没有做些记录,总觉得差点什么,所以这次趁我需要重装系统的机会,拍了些照片,结合一些文字来做一个教程吧。 阅读按照文章顺序来,不要跳着来,除非我注明让你可以跳过。

OSX系统

不做评价谁好谁差,但是作为 Web工程师 的话,在 osx 系统下的编程体验确实要比 Windows 下好很多,软件也特别干净,从来没有在你工作的时候,突然右下角来个弹窗给你看广告,或者直接把广告就弹在最顶层来影响你的工作。 jdk npm git Dart Flutter 这些程序,在 unix 系的 osx 下也从来没有报错。所以还是推荐使用的。

安装须知

台式机

在我看来台式机安装黑果是比较完美的,能够屏蔽独立显卡,没有 WiFi 的顾虑。而且较高的性能使得能够更加完美的体验 osx 系统。 但是还是有些限制你需要知道:

  1. Intel 芯支持比较完美,如果是 AMD 芯的话,虽然说现在有破解,但是我还是觉得不如自然免驱的好(无测试过,没什么发言权);
  2. 一般比较热门的独立显卡,在国内远景论坛都有驱动的教程,涉及一些代码的注入,我比较懒,所以公司的电脑就拆掉了独显,使用集显来驱动,集显也是免驱的;

笔记本

  1. 笔记本受限比较大,现在笔记本一般都是 集显+独显 来自动切换达到省电的功能的,但是白苹果的笔记本,不是一个独显就是一个集显。所以,两者混合的情况下一般只能驱动集显(除非部分笔记本可以屏蔽集显),显卡问题就是独显基本就废了;
  2. WiFi 常年无解,两个方法解决:
    1. 淘宝购买 usb 接口的免驱网卡,卖家通常会提供使用所需要的驱动和软件;
    2. 更换笔记本内部的网卡,这一步比较专业,加上联想笔记本更换配件是需要刷白名单的,所以通常来说,都会选择第一种方式解决。

安装流程

镜像下载

因为在这篇文章产生的时候最新的 osx 系统是 10.15 catalina。所以我就是用这个系统来做教程吧。 osx_10.15_catalina 下载地址:下载地址

感谢远景论坛大佬的封装,我没有直接放大佬的最终下载地址,因为我还是希望各位能够给去下载,大佬有硬币赚。

镜像已经下好了,大佬也提供了常用的机型配置:

这一步只是先让你们看,先放一边,我们通常安装的时候不会直接选择合适的驱动,而是先使用通用驱动来安装,他是以最小的通用兼容配置来做的,通常都能够进入安装并且顺利通过。

写入优盘

此时,你需要拥有一个 16G 的优盘!记住记住,一定是 16G 不然他镜像可以刻录但是不会帮你刻录完整,不完整的镜像就是一个无法安装的镜像。(我用了一个 8G 的在这一步卡了好几个小时,最后把公司的师弟杀了祭天了,他出现了警告也不看直接点击 OK 就给我刻录了一个废盘) 首先你需要一个软件:TransMac 当然因为我的习惯,所以我都是在 Windows 系统下写优盘的。 安装,然后右键 -> 以管理员身份运行

会有提示需要购买正版什么的,不过点击 run 可以直接运行试用版,试用版就够用了。 然后找到你的优盘,右键选择 Format Disk –> Format with Disk Image 即可。

漫长的等待,结束了就可以使用这个优盘来安装系统了。

修改BIOS引导

msruefi,只能选择后者。一般新买的电脑或者近几年的电脑都是这个。 简单的识别方法:开机的时候,如果是品牌 Logo 然后就开始转圈圈进入 Windows 系统的话,就是 uefi。 第二就是修改 biossecurity boot,直接关闭就完事了。如果同等级的还有说什么引导什么系统的话,记得把它修改成 other

如果需要双系统

如果不需要双系统安装,直接跳过这一段

双系统指的是 Windows + osx,好像一般初学者都需要这个东西(包括之前的我自己)? 首先首先,Windows 默认的引导分区太小了,如果你不重新安装 Windows 的话,直接安装 osx 后面都会导致任何软件都无法安装。 教程 类似于上面地址里面的教程,但是在创建 efi 的时候,大小给大点,我一般给的是 512m,因为后面我们可能还需要根据不同的机型,在这个分区里面放入驱动。 分好区,先把 Windows 装上,记得留一些硬盘给 osx,我建议是 100G

装好系统后,在硬盘管理那里给 efi 分区随便给个盘符,然后把里面的东西备份一份出来,因为待会需要对他开刀,备份完整不至于你总是需要重新安装系统。

优盘引导开机

为了安装方便我建议将 first boot 设置为上面的优盘。当然使用 F12 来选择也行不过中途需要重启的时候就要留意重新选择了。 选择优盘中的带有 UEFI 选项的,这时候电脑将会读取优盘的引导,进入引导界面。

进入优盘是这个样子的:

一般是在第一项,下面会提示 Boot macOS Install from xxx 后面一般是优盘它默认给的名字。此时,我们 需要按一次 空格键 ,来到下一个页面:

下面是启动选项,一般按下空格需要设定的,大佬已经帮我们写好了,这个时候直接 按下回车键 进入安装软件就可以了。 一串命令后,我们会进来到这个页面:

能够来到这里,说明安装过程十分顺利,接下来进入下一个环节。

硬盘分区

就跟上面一样,我们选择磁盘工具,然后点击 继续,就会进入到磁盘工具的页面,那我们先选择显示所有设备,这时候磁盘都会显示出来。

然后我们选择整个硬盘(如果是单系统的话,双系统的话,你需要选择你要格式化的那个分区)点击上面的抹掉(看我鼠标那个位置):

点击之后会出来这个硬盘设定界面:

  1. 名称,可以自定义;

  2. 格式,新出来的文件系统是 APFS 听说是提速以及增加安全性的,那我就用这个好了,也建议使用这个。强烈不要使用区分大小写的分区格式(敲黑板),因为我使用了大小写区分的硬盘导致有些软件它不支持!!我目前就是因为这个原因重新安装系统的;

  3. 方案选择 GUID 分区图

稍稍等待一番,成功以后,直接左上角关闭磁盘管理工具。我们就可以安装系统了。

安装系统

回到这个界面,选择 安装 macOS


然后就同意协议了,这个不用图都知道要选择什么吧… 同意协议之后就是选择硬盘了,在这个界面,没有意外的话应该可以看到我们刚刚分出来的硬盘分区:

选好硬盘点击安装,接下来进入漫长的等待:

!!!请注意!!!几分钟动一次鼠标,不要让电脑进入睡眠,因为可能因为驱动原因会睡死。 然后系统在安装过程需要一次重启,别担心,如果你在之前已经设置好优盘第一启动了的话,就不用管它,没有的话,需要手动选择优盘引导,进入优盘不用做什么,会继续安装:

跑完基本就可以进入系统的基础设置页面了:

选择什么不用说了吧,选择其他国家也可以,只要看得懂就好…

完成安装

完成安装可以进入桌面了,左上角有个 -> 关于本机

哎哟卧槽,这个显存不对劲啊….. 那现在我们暂时忍一下,使用憋屈的动画效果使用一下 Safari,去下载一个软件:Clover Configurator 安装软件

LaunchPad 打开运行,不出意外的话:

LaunchPad/其他/终端 运行以下命令,输入设置的用户密码就可以了:

1
sudo spctl --master-disable

重新打开 Clover Configurator 来到这个菜单:

我们现在的引导还是优盘,所以先在优盘那一项点击后面的挂载,这时候应该需要输入用户的密码,挂载成功完以后,就可以在 访达 看到 EFI 分区了。

还记得这张图吗:

将原来的

config.plist 备份一份,把这里面的适合自己机型的配置,改名为 config.plist 放在上一层文件夹:

然后重启电脑,进入优盘引导,根据下面字样提示,选择 Boot macOS from 你设置的分区名 即可进入安装好的系统。如果配置兼容的话,重新查看关于本机:

显存正常了。 再尝试音乐,如果声卡也正常,那就完成安装了。

迁入硬盘引导

一切好了,总不能总是插着硬盘来开机吧,我们需要把硬盘的文件替换到磁盘里面去,还是使用 Clover Configurator

一个是硬盘的,一个是优盘的,同时挂载出来。 然后使用 访达 进行操作:

OK,bios 重新设置第一启动是硬盘,然后,如果需要进入 osx 则选择 osx 硬盘回车,需要 Windows 的话就选择 Windows 所在的分区即可。

其他设置

这是我自己觉得比较好的设置,每个人可以按照自己的习惯来。 当然关于原来在 Windows 习惯的 Ctrl + c 这些都需要换成 Command + C 了,Command 在原来的 Win键 上面,我是不建议在系统设置里面替换这两者的按键位置的,毕竟用起来还蛮舒服的。 然后所有软件需要进入设置界面,都已经统一是 Command + , 这个快捷键了。

访达小设置

访达默认设置我感觉是不太好用的,比如点击访达,显示的不是我的电脑的内容,而是最近使用的内容,我特么写代码最近使用的内容都是代码文件,而我们通常不需要在这里去访问这些文件的(都是通过 IDE 工具),所以我希望点击访达进来的时候就是 我的个人文件夹: 打开 访达Command + ,

第二个不好的点是,我去到那个文件夹,要搜索某个文件,默认给我跑整个电脑去搜索,速度慢又没必要,所以:

第三个不好的点是,文件默认居然不排序,还要像垃圾堆一样堆在一起!!改掉设置!: 这个不在 Command + , 里边了,在这里:

开启Retina渲染

参考资料: 黑苹果开启缩放分辨率HiDPi以及字体模糊的调整方法总结 黑苹果开启HiDPI“解决”字体模糊的问题 直接在 其他软件/终端 粘贴下面的命令运行,提示输入用户密码,输入完以后根据提示来走就可以了:

1
sh -c "$(curl -fsSL https://raw.githubusercontent.com/xzhih/one-key-hidpi/master/hidpi.sh)"

重启电脑后安装 RDM 工具来配置分辨率(下载 RDM-2.2.dmg 就可以了 ) 运行,右上角:

选择带有小闪电的就是 HiDPI 的设置(也就是 Retina 效果)

一般根据屏幕原始分辨率来定,有 4K 显示器开 2K 分辨率的话,效果就跟白苹果一样了,2倍的差距,我目前原始分辨率是 1920 * 1080,开图中的分辨率也就凑合凑合过吧

然后在 系统偏好设置 –> 通用 里面开启字体平滑(在最下面,默认也是开启的)

我对默认的平滑程度还是不太喜欢,要开最高的,终端运行以下命令,提示输入用户密码,输入完以后重启即可:

1
defaults -currentHost write -globalDomain AppleFontSmoothing -int 3

来看看效果图: 开启前: 开启后:

结束

后面有空再更更常用软件了。

然后重启电脑,进入优盘引导,根据下面字样提示,选择 Boot macOS from 你设置的分区名 即可进入安装好的系统。如果配置兼容的话,重新查看关于本机:

从大学就开始折腾黑苹果,迟迟没有做些记录,总觉得差点什么,所以这次趁我需要重装系统的机会,拍了些照片,结合一些文字来做一个教程吧。 阅读按照文章顺序来,不要跳着来,除非我注明让你可以跳过。

OSX系统

不做评价谁好谁差,但是作为 Web工程师 的话,在 osx 系统下的编程体验确实要比 Windows 下好很多,软件也特别干净,从来没有在你工作的时候,突然右下角来个弹窗给你看广告,或者直接把广告就弹在最顶层来影响你的工作。 jdk npm git Dart Flutter 这些程序,在 unix 系的 osx 下也从来没有报错。所以还是推荐使用的。

安装须知

台式机

在我看来台式机安装黑果是比较完美的,能够屏蔽独立显卡,没有 WiFi 的顾虑。而且较高的性能使得能够更加完美的体验 osx 系统。 但是还是有些限制你需要知道:

  1. Intel 芯支持比较完美,如果是 AMD 芯的话,虽然说现在有破解,但是我还是觉得不如自然免驱的好(无测试过,没什么发言权);
  2. 一般比较热门的独立显卡,在国内远景论坛都有驱动的教程,涉及一些代码的注入,我比较懒,所以公司的电脑就拆掉了独显,使用集显来驱动,集显也是免驱的;

笔记本

  1. 笔记本受限比较大,现在笔记本一般都是 集显+独显 来自动切换达到省电的功能的,但是白苹果的笔记本,不是一个独显就是一个集显。所以,两者混合的情况下一般只能驱动集显(除非部分笔记本可以屏蔽集显),显卡问题就是独显基本就废了;
  2. WiFi 常年无解,两个方法解决:
    1. 淘宝购买 usb 接口的免驱网卡,卖家通常会提供使用所需要的驱动和软件;
    2. 更换笔记本内部的网卡,这一步比较专业,加上联想笔记本更换配件是需要刷白名单的,所以通常来说,都会选择第一种方式解决。

安装流程

镜像下载

因为在这篇文章产生的时候最新的 osx 系统是 10.15 catalina。所以我就是用这个系统来做教程吧。 osx_10.15_catalina 下载地址:下载地址

感谢远景论坛大佬的封装,我没有直接放大佬的最终下载地址,因为我还是希望各位能够给去下载,大佬有硬币赚。

镜像已经下好了,大佬也提供了常用的机型配置:

这一步只是先让你们看,先放一边,我们通常安装的时候不会直接选择合适的驱动,而是先使用通用驱动来安装,他是以最小的通用兼容配置来做的,通常都能够进入安装并且顺利通过。

写入优盘

此时,你需要拥有一个 16G 的优盘!记住记住,一定是 16G 不然他镜像可以刻录但是不会帮你刻录完整,不完整的镜像就是一个无法安装的镜像。(我用了一个 8G 的在这一步卡了好几个小时,最后把公司的师弟杀了祭天了,他出现了警告也不看直接点击 OK 就给我刻录了一个废盘) 首先你需要一个软件:TransMac 当然因为我的习惯,所以我都是在 Windows 系统下写优盘的。 安装,然后右键 -> 以管理员身份运行

会有提示需要购买正版什么的,不过点击 run 可以直接运行试用版,试用版就够用了。 然后找到你的优盘,右键选择 Format Disk –> Format with Disk Image 即可。

漫长的等待,结束了就可以使用这个优盘来安装系统了。

修改BIOS引导

msruefi,只能选择后者。一般新买的电脑或者近几年的电脑都是这个。 简单的识别方法:开机的时候,如果是品牌 Logo 然后就开始转圈圈进入 Windows 系统的话,就是 uefi。 第二就是修改 biossecurity boot,直接关闭就完事了。如果同等级的还有说什么引导什么系统的话,记得把它修改成 other

如果需要双系统

如果不需要双系统安装,直接跳过这一段

双系统指的是 Windows + osx,好像一般初学者都需要这个东西(包括之前的我自己)? 首先首先,Windows 默认的引导分区太小了,如果你不重新安装 Windows 的话,直接安装 osx 后面都会导致任何软件都无法安装。 教程 类似于上面地址里面的教程,但是在创建 efi 的时候,大小给大点,我一般给的是 512m,因为后面我们可能还需要根据不同的机型,在这个分区里面放入驱动。 分好区,先把 Windows 装上,记得留一些硬盘给 osx,我建议是 100G

装好系统后,在硬盘管理那里给 efi 分区随便给个盘符,然后把里面的东西备份一份出来,因为待会需要对他开刀,备份完整不至于你总是需要重新安装系统。

优盘引导开机

为了安装方便我建议将 first boot 设置为上面的优盘。当然使用 F12 来选择也行不过中途需要重启的时候就要留意重新选择了。 选择优盘中的带有 UEFI 选项的,这时候电脑将会读取优盘的引导,进入引导界面。

进入优盘是这个样子的:

一般是在第一项,下面会提示 Boot macOS Install from xxx 后面一般是优盘它默认给的名字。此时,我们 需要按一次 空格键 ,来到下一个页面:

下面是启动选项,一般按下空格需要设定的,大佬已经帮我们写好了,这个时候直接 按下回车键 进入安装软件就可以了。 一串命令后,我们会进来到这个页面:

能够来到这里,说明安装过程十分顺利,接下来进入下一个环节。

硬盘分区

就跟上面一样,我们选择磁盘工具,然后点击 继续,就会进入到磁盘工具的页面,那我们先选择显示所有设备,这时候磁盘都会显示出来。

然后我们选择整个硬盘(如果是单系统的话,双系统的话,你需要选择你要格式化的那个分区)点击上面的抹掉(看我鼠标那个位置):

点击之后会出来这个硬盘设定界面:

  1. 名称,可以自定义;

  2. 格式,新出来的文件系统是 APFS 听说是提速以及增加安全性的,那我就用这个好了,也建议使用这个。强烈不要使用区分大小写的分区格式(敲黑板),因为我使用了大小写区分的硬盘导致有些软件它不支持!!我目前就是因为这个原因重新安装系统的;

  3. 方案选择 GUID 分区图

稍稍等待一番,成功以后,直接左上角关闭磁盘管理工具。我们就可以安装系统了。

安装系统

回到这个界面,选择 安装 macOS


然后就同意协议了,这个不用图都知道要选择什么吧… 同意协议之后就是选择硬盘了,在这个界面,没有意外的话应该可以看到我们刚刚分出来的硬盘分区:

选好硬盘点击安装,接下来进入漫长的等待:

!!!请注意!!!几分钟动一次鼠标,不要让电脑进入睡眠,因为可能因为驱动原因会睡死。 然后系统在安装过程需要一次重启,别担心,如果你在之前已经设置好优盘第一启动了的话,就不用管它,没有的话,需要手动选择优盘引导,进入优盘不用做什么,会继续安装:

跑完基本就可以进入系统的基础设置页面了:

选择什么不用说了吧,选择其他国家也可以,只要看得懂就好…

完成安装

完成安装可以进入桌面了,左上角有个 -> 关于本机

哎哟卧槽,这个显存不对劲啊….. 那现在我们暂时忍一下,使用憋屈的动画效果使用一下 Safari,去下载一个软件:Clover Configurator 安装软件

LaunchPad 打开运行,不出意外的话:

LaunchPad/其他/终端 运行以下命令,输入设置的用户密码就可以了:

1
sudo spctl --master-disable

重新打开 Clover Configurator 来到这个菜单:

我们现在的引导还是优盘,所以先在优盘那一项点击后面的挂载,这时候应该需要输入用户的密码,挂载成功完以后,就可以在 访达 看到 EFI 分区了。

还记得这张图吗:

将原来的

config.plist 备份一份,把这里面的适合自己机型的配置,改名为 config.plist 放在上一层文件夹:

然后重启电脑,进入优盘引导,根据下面字样提示,选择 Boot macOS from 你设置的分区名 即可进入安装好的系统。如果配置兼容的话,重新查看关于本机:

显存正常了。 再尝试音乐,如果声卡也正常,那就完成安装了。

迁入硬盘引导

一切好了,总不能总是插着硬盘来开机吧,我们需要把硬盘的文件替换到磁盘里面去,还是使用 Clover Configurator

一个是硬盘的,一个是优盘的,同时挂载出来。 然后使用 访达 进行操作:

OK,bios 重新设置第一启动是硬盘,然后,如果需要进入 osx 则选择 osx 硬盘回车,需要 Windows 的话就选择 Windows 所在的分区即可。

其他设置

这是我自己觉得比较好的设置,每个人可以按照自己的习惯来。 当然关于原来在 Windows 习惯的 Ctrl + c 这些都需要换成 Command + C 了,Command 在原来的 Win键 上面,我是不建议在系统设置里面替换这两者的按键位置的,毕竟用起来还蛮舒服的。 然后所有软件需要进入设置界面,都已经统一是 Command + , 这个快捷键了。

访达小设置

访达默认设置我感觉是不太好用的,比如点击访达,显示的不是我的电脑的内容,而是最近使用的内容,我特么写代码最近使用的内容都是代码文件,而我们通常不需要在这里去访问这些文件的(都是通过 IDE 工具),所以我希望点击访达进来的时候就是 我的个人文件夹: 打开 访达Command + ,

第二个不好的点是,我去到那个文件夹,要搜索某个文件,默认给我跑整个电脑去搜索,速度慢又没必要,所以:

第三个不好的点是,文件默认居然不排序,还要像垃圾堆一样堆在一起!!改掉设置!: 这个不在 Command + , 里边了,在这里:

开启Retina渲染

参考资料: 黑苹果开启缩放分辨率HiDPi以及字体模糊的调整方法总结 黑苹果开启HiDPI“解决”字体模糊的问题 直接在 其他软件/终端 粘贴下面的命令运行,提示输入用户密码,输入完以后根据提示来走就可以了:

1
sh -c "$(curl -fsSL https://raw.githubusercontent.com/xzhih/one-key-hidpi/master/hidpi.sh)"

重启电脑后安装 RDM 工具来配置分辨率(下载 RDM-2.2.dmg 就可以了 ) 运行,右上角:

选择带有小闪电的就是 HiDPI 的设置(也就是 Retina 效果)

一般根据屏幕原始分辨率来定,有 4K 显示器开 2K 分辨率的话,效果就跟白苹果一样了,2倍的差距,我目前原始分辨率是 1920 * 1080,开图中的分辨率也就凑合凑合过吧

然后在 系统偏好设置 –> 通用 里面开启字体平滑(在最下面,默认也是开启的)

我对默认的平滑程度还是不太喜欢,要开最高的,终端运行以下命令,提示输入用户密码,输入完以后重启即可:

1
defaults -currentHost write -globalDomain AppleFontSmoothing -int 3

来看看效果图: 开启前: 开启后:

结束

后面有空再更更常用软件了。

显存正常了。 再尝试音乐,如果声卡也正常,那就完成安装了。

迁入硬盘引导

一切好了,总不能总是插着硬盘来开机吧,我们需要把硬盘的文件替换到磁盘里面去,还是使用 Clover Configurator

从大学就开始折腾黑苹果,迟迟没有做些记录,总觉得差点什么,所以这次趁我需要重装系统的机会,拍了些照片,结合一些文字来做一个教程吧。 阅读按照文章顺序来,不要跳着来,除非我注明让你可以跳过。

OSX系统

不做评价谁好谁差,但是作为 Web工程师 的话,在 osx 系统下的编程体验确实要比 Windows 下好很多,软件也特别干净,从来没有在你工作的时候,突然右下角来个弹窗给你看广告,或者直接把广告就弹在最顶层来影响你的工作。 jdk npm git Dart Flutter 这些程序,在 unix 系的 osx 下也从来没有报错。所以还是推荐使用的。

安装须知

台式机

在我看来台式机安装黑果是比较完美的,能够屏蔽独立显卡,没有 WiFi 的顾虑。而且较高的性能使得能够更加完美的体验 osx 系统。 但是还是有些限制你需要知道:

  1. Intel 芯支持比较完美,如果是 AMD 芯的话,虽然说现在有破解,但是我还是觉得不如自然免驱的好(无测试过,没什么发言权);
  2. 一般比较热门的独立显卡,在国内远景论坛都有驱动的教程,涉及一些代码的注入,我比较懒,所以公司的电脑就拆掉了独显,使用集显来驱动,集显也是免驱的;

笔记本

  1. 笔记本受限比较大,现在笔记本一般都是 集显+独显 来自动切换达到省电的功能的,但是白苹果的笔记本,不是一个独显就是一个集显。所以,两者混合的情况下一般只能驱动集显(除非部分笔记本可以屏蔽集显),显卡问题就是独显基本就废了;
  2. WiFi 常年无解,两个方法解决:
    1. 淘宝购买 usb 接口的免驱网卡,卖家通常会提供使用所需要的驱动和软件;
    2. 更换笔记本内部的网卡,这一步比较专业,加上联想笔记本更换配件是需要刷白名单的,所以通常来说,都会选择第一种方式解决。

安装流程

镜像下载

因为在这篇文章产生的时候最新的 osx 系统是 10.15 catalina。所以我就是用这个系统来做教程吧。 osx_10.15_catalina 下载地址:下载地址

感谢远景论坛大佬的封装,我没有直接放大佬的最终下载地址,因为我还是希望各位能够给去下载,大佬有硬币赚。

镜像已经下好了,大佬也提供了常用的机型配置:

这一步只是先让你们看,先放一边,我们通常安装的时候不会直接选择合适的驱动,而是先使用通用驱动来安装,他是以最小的通用兼容配置来做的,通常都能够进入安装并且顺利通过。

写入优盘

此时,你需要拥有一个 16G 的优盘!记住记住,一定是 16G 不然他镜像可以刻录但是不会帮你刻录完整,不完整的镜像就是一个无法安装的镜像。(我用了一个 8G 的在这一步卡了好几个小时,最后把公司的师弟杀了祭天了,他出现了警告也不看直接点击 OK 就给我刻录了一个废盘) 首先你需要一个软件:TransMac 当然因为我的习惯,所以我都是在 Windows 系统下写优盘的。 安装,然后右键 -> 以管理员身份运行

会有提示需要购买正版什么的,不过点击 run 可以直接运行试用版,试用版就够用了。 然后找到你的优盘,右键选择 Format Disk –> Format with Disk Image 即可。

漫长的等待,结束了就可以使用这个优盘来安装系统了。

修改BIOS引导

msruefi,只能选择后者。一般新买的电脑或者近几年的电脑都是这个。 简单的识别方法:开机的时候,如果是品牌 Logo 然后就开始转圈圈进入 Windows 系统的话,就是 uefi。 第二就是修改 biossecurity boot,直接关闭就完事了。如果同等级的还有说什么引导什么系统的话,记得把它修改成 other

如果需要双系统

如果不需要双系统安装,直接跳过这一段

双系统指的是 Windows + osx,好像一般初学者都需要这个东西(包括之前的我自己)? 首先首先,Windows 默认的引导分区太小了,如果你不重新安装 Windows 的话,直接安装 osx 后面都会导致任何软件都无法安装。 教程 类似于上面地址里面的教程,但是在创建 efi 的时候,大小给大点,我一般给的是 512m,因为后面我们可能还需要根据不同的机型,在这个分区里面放入驱动。 分好区,先把 Windows 装上,记得留一些硬盘给 osx,我建议是 100G

装好系统后,在硬盘管理那里给 efi 分区随便给个盘符,然后把里面的东西备份一份出来,因为待会需要对他开刀,备份完整不至于你总是需要重新安装系统。

优盘引导开机

为了安装方便我建议将 first boot 设置为上面的优盘。当然使用 F12 来选择也行不过中途需要重启的时候就要留意重新选择了。 选择优盘中的带有 UEFI 选项的,这时候电脑将会读取优盘的引导,进入引导界面。

进入优盘是这个样子的:

一般是在第一项,下面会提示 Boot macOS Install from xxx 后面一般是优盘它默认给的名字。此时,我们 需要按一次 空格键 ,来到下一个页面:

下面是启动选项,一般按下空格需要设定的,大佬已经帮我们写好了,这个时候直接 按下回车键 进入安装软件就可以了。 一串命令后,我们会进来到这个页面:

能够来到这里,说明安装过程十分顺利,接下来进入下一个环节。

硬盘分区

就跟上面一样,我们选择磁盘工具,然后点击 继续,就会进入到磁盘工具的页面,那我们先选择显示所有设备,这时候磁盘都会显示出来。

然后我们选择整个硬盘(如果是单系统的话,双系统的话,你需要选择你要格式化的那个分区)点击上面的抹掉(看我鼠标那个位置):

点击之后会出来这个硬盘设定界面:

  1. 名称,可以自定义;

  2. 格式,新出来的文件系统是 APFS 听说是提速以及增加安全性的,那我就用这个好了,也建议使用这个。强烈不要使用区分大小写的分区格式(敲黑板),因为我使用了大小写区分的硬盘导致有些软件它不支持!!我目前就是因为这个原因重新安装系统的;

  3. 方案选择 GUID 分区图

稍稍等待一番,成功以后,直接左上角关闭磁盘管理工具。我们就可以安装系统了。

安装系统

回到这个界面,选择 安装 macOS


然后就同意协议了,这个不用图都知道要选择什么吧… 同意协议之后就是选择硬盘了,在这个界面,没有意外的话应该可以看到我们刚刚分出来的硬盘分区:

选好硬盘点击安装,接下来进入漫长的等待:

!!!请注意!!!几分钟动一次鼠标,不要让电脑进入睡眠,因为可能因为驱动原因会睡死。 然后系统在安装过程需要一次重启,别担心,如果你在之前已经设置好优盘第一启动了的话,就不用管它,没有的话,需要手动选择优盘引导,进入优盘不用做什么,会继续安装:

跑完基本就可以进入系统的基础设置页面了:

选择什么不用说了吧,选择其他国家也可以,只要看得懂就好…

完成安装

完成安装可以进入桌面了,左上角有个 -> 关于本机

哎哟卧槽,这个显存不对劲啊….. 那现在我们暂时忍一下,使用憋屈的动画效果使用一下 Safari,去下载一个软件:Clover Configurator 安装软件

LaunchPad 打开运行,不出意外的话:

LaunchPad/其他/终端 运行以下命令,输入设置的用户密码就可以了:

1
sudo spctl --master-disable

重新打开 Clover Configurator 来到这个菜单:

我们现在的引导还是优盘,所以先在优盘那一项点击后面的挂载,这时候应该需要输入用户的密码,挂载成功完以后,就可以在 访达 看到 EFI 分区了。

还记得这张图吗:

将原来的

config.plist 备份一份,把这里面的适合自己机型的配置,改名为 config.plist 放在上一层文件夹:

然后重启电脑,进入优盘引导,根据下面字样提示,选择 Boot macOS from 你设置的分区名 即可进入安装好的系统。如果配置兼容的话,重新查看关于本机:

显存正常了。 再尝试音乐,如果声卡也正常,那就完成安装了。

迁入硬盘引导

一切好了,总不能总是插着硬盘来开机吧,我们需要把硬盘的文件替换到磁盘里面去,还是使用 Clover Configurator

一个是硬盘的,一个是优盘的,同时挂载出来。 然后使用 访达 进行操作:

OK,bios 重新设置第一启动是硬盘,然后,如果需要进入 osx 则选择 osx 硬盘回车,需要 Windows 的话就选择 Windows 所在的分区即可。

其他设置

这是我自己觉得比较好的设置,每个人可以按照自己的习惯来。 当然关于原来在 Windows 习惯的 Ctrl + c 这些都需要换成 Command + C 了,Command 在原来的 Win键 上面,我是不建议在系统设置里面替换这两者的按键位置的,毕竟用起来还蛮舒服的。 然后所有软件需要进入设置界面,都已经统一是 Command + , 这个快捷键了。

访达小设置

访达默认设置我感觉是不太好用的,比如点击访达,显示的不是我的电脑的内容,而是最近使用的内容,我特么写代码最近使用的内容都是代码文件,而我们通常不需要在这里去访问这些文件的(都是通过 IDE 工具),所以我希望点击访达进来的时候就是 我的个人文件夹: 打开 访达Command + ,

第二个不好的点是,我去到那个文件夹,要搜索某个文件,默认给我跑整个电脑去搜索,速度慢又没必要,所以:

第三个不好的点是,文件默认居然不排序,还要像垃圾堆一样堆在一起!!改掉设置!: 这个不在 Command + , 里边了,在这里:

开启Retina渲染

参考资料: 黑苹果开启缩放分辨率HiDPi以及字体模糊的调整方法总结 黑苹果开启HiDPI“解决”字体模糊的问题 直接在 其他软件/终端 粘贴下面的命令运行,提示输入用户密码,输入完以后根据提示来走就可以了:

1
sh -c "$(curl -fsSL https://raw.githubusercontent.com/xzhih/one-key-hidpi/master/hidpi.sh)"

重启电脑后安装 RDM 工具来配置分辨率(下载 RDM-2.2.dmg 就可以了 ) 运行,右上角:

选择带有小闪电的就是 HiDPI 的设置(也就是 Retina 效果)

一般根据屏幕原始分辨率来定,有 4K 显示器开 2K 分辨率的话,效果就跟白苹果一样了,2倍的差距,我目前原始分辨率是 1920 * 1080,开图中的分辨率也就凑合凑合过吧

然后在 系统偏好设置 –> 通用 里面开启字体平滑(在最下面,默认也是开启的)

我对默认的平滑程度还是不太喜欢,要开最高的,终端运行以下命令,提示输入用户密码,输入完以后重启即可:

1
defaults -currentHost write -globalDomain AppleFontSmoothing -int 3

来看看效果图: 开启前: 开启后:

结束

后面有空再更更常用软件了。

一个是硬盘的,一个是优盘的,同时挂载出来。 然后使用 访达 进行操作:

从大学就开始折腾黑苹果,迟迟没有做些记录,总觉得差点什么,所以这次趁我需要重装系统的机会,拍了些照片,结合一些文字来做一个教程吧。 阅读按照文章顺序来,不要跳着来,除非我注明让你可以跳过。

OSX系统

不做评价谁好谁差,但是作为 Web工程师 的话,在 osx 系统下的编程体验确实要比 Windows 下好很多,软件也特别干净,从来没有在你工作的时候,突然右下角来个弹窗给你看广告,或者直接把广告就弹在最顶层来影响你的工作。 jdk npm git Dart Flutter 这些程序,在 unix 系的 osx 下也从来没有报错。所以还是推荐使用的。

安装须知

台式机

在我看来台式机安装黑果是比较完美的,能够屏蔽独立显卡,没有 WiFi 的顾虑。而且较高的性能使得能够更加完美的体验 osx 系统。 但是还是有些限制你需要知道:

  1. Intel 芯支持比较完美,如果是 AMD 芯的话,虽然说现在有破解,但是我还是觉得不如自然免驱的好(无测试过,没什么发言权);
  2. 一般比较热门的独立显卡,在国内远景论坛都有驱动的教程,涉及一些代码的注入,我比较懒,所以公司的电脑就拆掉了独显,使用集显来驱动,集显也是免驱的;

笔记本

  1. 笔记本受限比较大,现在笔记本一般都是 集显+独显 来自动切换达到省电的功能的,但是白苹果的笔记本,不是一个独显就是一个集显。所以,两者混合的情况下一般只能驱动集显(除非部分笔记本可以屏蔽集显),显卡问题就是独显基本就废了;
  2. WiFi 常年无解,两个方法解决:
    1. 淘宝购买 usb 接口的免驱网卡,卖家通常会提供使用所需要的驱动和软件;
    2. 更换笔记本内部的网卡,这一步比较专业,加上联想笔记本更换配件是需要刷白名单的,所以通常来说,都会选择第一种方式解决。

安装流程

镜像下载

因为在这篇文章产生的时候最新的 osx 系统是 10.15 catalina。所以我就是用这个系统来做教程吧。 osx_10.15_catalina 下载地址:下载地址

感谢远景论坛大佬的封装,我没有直接放大佬的最终下载地址,因为我还是希望各位能够给去下载,大佬有硬币赚。

镜像已经下好了,大佬也提供了常用的机型配置:

这一步只是先让你们看,先放一边,我们通常安装的时候不会直接选择合适的驱动,而是先使用通用驱动来安装,他是以最小的通用兼容配置来做的,通常都能够进入安装并且顺利通过。

写入优盘

此时,你需要拥有一个 16G 的优盘!记住记住,一定是 16G 不然他镜像可以刻录但是不会帮你刻录完整,不完整的镜像就是一个无法安装的镜像。(我用了一个 8G 的在这一步卡了好几个小时,最后把公司的师弟杀了祭天了,他出现了警告也不看直接点击 OK 就给我刻录了一个废盘) 首先你需要一个软件:TransMac 当然因为我的习惯,所以我都是在 Windows 系统下写优盘的。 安装,然后右键 -> 以管理员身份运行

会有提示需要购买正版什么的,不过点击 run 可以直接运行试用版,试用版就够用了。 然后找到你的优盘,右键选择 Format Disk –> Format with Disk Image 即可。

漫长的等待,结束了就可以使用这个优盘来安装系统了。

修改BIOS引导

msruefi,只能选择后者。一般新买的电脑或者近几年的电脑都是这个。 简单的识别方法:开机的时候,如果是品牌 Logo 然后就开始转圈圈进入 Windows 系统的话,就是 uefi。 第二就是修改 biossecurity boot,直接关闭就完事了。如果同等级的还有说什么引导什么系统的话,记得把它修改成 other

如果需要双系统

如果不需要双系统安装,直接跳过这一段

双系统指的是 Windows + osx,好像一般初学者都需要这个东西(包括之前的我自己)? 首先首先,Windows 默认的引导分区太小了,如果你不重新安装 Windows 的话,直接安装 osx 后面都会导致任何软件都无法安装。 教程 类似于上面地址里面的教程,但是在创建 efi 的时候,大小给大点,我一般给的是 512m,因为后面我们可能还需要根据不同的机型,在这个分区里面放入驱动。 分好区,先把 Windows 装上,记得留一些硬盘给 osx,我建议是 100G

装好系统后,在硬盘管理那里给 efi 分区随便给个盘符,然后把里面的东西备份一份出来,因为待会需要对他开刀,备份完整不至于你总是需要重新安装系统。

优盘引导开机

为了安装方便我建议将 first boot 设置为上面的优盘。当然使用 F12 来选择也行不过中途需要重启的时候就要留意重新选择了。 选择优盘中的带有 UEFI 选项的,这时候电脑将会读取优盘的引导,进入引导界面。

进入优盘是这个样子的:

一般是在第一项,下面会提示 Boot macOS Install from xxx 后面一般是优盘它默认给的名字。此时,我们 需要按一次 空格键 ,来到下一个页面:

下面是启动选项,一般按下空格需要设定的,大佬已经帮我们写好了,这个时候直接 按下回车键 进入安装软件就可以了。 一串命令后,我们会进来到这个页面:

能够来到这里,说明安装过程十分顺利,接下来进入下一个环节。

硬盘分区

就跟上面一样,我们选择磁盘工具,然后点击 继续,就会进入到磁盘工具的页面,那我们先选择显示所有设备,这时候磁盘都会显示出来。

然后我们选择整个硬盘(如果是单系统的话,双系统的话,你需要选择你要格式化的那个分区)点击上面的抹掉(看我鼠标那个位置):

点击之后会出来这个硬盘设定界面:

  1. 名称,可以自定义;

  2. 格式,新出来的文件系统是 APFS 听说是提速以及增加安全性的,那我就用这个好了,也建议使用这个。强烈不要使用区分大小写的分区格式(敲黑板),因为我使用了大小写区分的硬盘导致有些软件它不支持!!我目前就是因为这个原因重新安装系统的;

  3. 方案选择 GUID 分区图

稍稍等待一番,成功以后,直接左上角关闭磁盘管理工具。我们就可以安装系统了。

安装系统

回到这个界面,选择 安装 macOS


然后就同意协议了,这个不用图都知道要选择什么吧… 同意协议之后就是选择硬盘了,在这个界面,没有意外的话应该可以看到我们刚刚分出来的硬盘分区:

选好硬盘点击安装,接下来进入漫长的等待:

!!!请注意!!!几分钟动一次鼠标,不要让电脑进入睡眠,因为可能因为驱动原因会睡死。 然后系统在安装过程需要一次重启,别担心,如果你在之前已经设置好优盘第一启动了的话,就不用管它,没有的话,需要手动选择优盘引导,进入优盘不用做什么,会继续安装:

跑完基本就可以进入系统的基础设置页面了:

选择什么不用说了吧,选择其他国家也可以,只要看得懂就好…

完成安装

完成安装可以进入桌面了,左上角有个 -> 关于本机

哎哟卧槽,这个显存不对劲啊….. 那现在我们暂时忍一下,使用憋屈的动画效果使用一下 Safari,去下载一个软件:Clover Configurator 安装软件

LaunchPad 打开运行,不出意外的话:

LaunchPad/其他/终端 运行以下命令,输入设置的用户密码就可以了:

1
sudo spctl --master-disable

重新打开 Clover Configurator 来到这个菜单:

我们现在的引导还是优盘,所以先在优盘那一项点击后面的挂载,这时候应该需要输入用户的密码,挂载成功完以后,就可以在 访达 看到 EFI 分区了。

还记得这张图吗:

将原来的

config.plist 备份一份,把这里面的适合自己机型的配置,改名为 config.plist 放在上一层文件夹:

然后重启电脑,进入优盘引导,根据下面字样提示,选择 Boot macOS from 你设置的分区名 即可进入安装好的系统。如果配置兼容的话,重新查看关于本机:

显存正常了。 再尝试音乐,如果声卡也正常,那就完成安装了。

迁入硬盘引导

一切好了,总不能总是插着硬盘来开机吧,我们需要把硬盘的文件替换到磁盘里面去,还是使用 Clover Configurator

一个是硬盘的,一个是优盘的,同时挂载出来。 然后使用 访达 进行操作:

OK,bios 重新设置第一启动是硬盘,然后,如果需要进入 osx 则选择 osx 硬盘回车,需要 Windows 的话就选择 Windows 所在的分区即可。

其他设置

这是我自己觉得比较好的设置,每个人可以按照自己的习惯来。 当然关于原来在 Windows 习惯的 Ctrl + c 这些都需要换成 Command + C 了,Command 在原来的 Win键 上面,我是不建议在系统设置里面替换这两者的按键位置的,毕竟用起来还蛮舒服的。 然后所有软件需要进入设置界面,都已经统一是 Command + , 这个快捷键了。

访达小设置

访达默认设置我感觉是不太好用的,比如点击访达,显示的不是我的电脑的内容,而是最近使用的内容,我特么写代码最近使用的内容都是代码文件,而我们通常不需要在这里去访问这些文件的(都是通过 IDE 工具),所以我希望点击访达进来的时候就是 我的个人文件夹: 打开 访达Command + ,

第二个不好的点是,我去到那个文件夹,要搜索某个文件,默认给我跑整个电脑去搜索,速度慢又没必要,所以:

第三个不好的点是,文件默认居然不排序,还要像垃圾堆一样堆在一起!!改掉设置!: 这个不在 Command + , 里边了,在这里:

开启Retina渲染

参考资料: 黑苹果开启缩放分辨率HiDPi以及字体模糊的调整方法总结 黑苹果开启HiDPI“解决”字体模糊的问题 直接在 其他软件/终端 粘贴下面的命令运行,提示输入用户密码,输入完以后根据提示来走就可以了:

1
sh -c "$(curl -fsSL https://raw.githubusercontent.com/xzhih/one-key-hidpi/master/hidpi.sh)"

重启电脑后安装 RDM 工具来配置分辨率(下载 RDM-2.2.dmg 就可以了 ) 运行,右上角:

选择带有小闪电的就是 HiDPI 的设置(也就是 Retina 效果)

一般根据屏幕原始分辨率来定,有 4K 显示器开 2K 分辨率的话,效果就跟白苹果一样了,2倍的差距,我目前原始分辨率是 1920 * 1080,开图中的分辨率也就凑合凑合过吧

然后在 系统偏好设置 –> 通用 里面开启字体平滑(在最下面,默认也是开启的)

我对默认的平滑程度还是不太喜欢,要开最高的,终端运行以下命令,提示输入用户密码,输入完以后重启即可:

1
defaults -currentHost write -globalDomain AppleFontSmoothing -int 3

来看看效果图: 开启前: 开启后:

结束

后面有空再更更常用软件了。 OK,bios 重新设置第一启动是硬盘,然后,如果需要进入 osx 则选择 osx 硬盘回车,需要 Windows 的话就选择 Windows 所在的分区即可。

其他设置

这是我自己觉得比较好的设置,每个人可以按照自己的习惯来。 当然关于原来在 Windows 习惯的 Ctrl + c 这些都需要换成 Command + C 了,Command 在原来的 Win键 上面,我是不建议在系统设置里面替换这两者的按键位置的,毕竟用起来还蛮舒服的。 然后所有软件需要进入设置界面,都已经统一是 Command + , 这个快捷键了。

访达小设置

访达默认设置我感觉是不太好用的,比如点击访达,显示的不是我的电脑的内容,而是最近使用的内容,我特么写代码最近使用的内容都是代码文件,而我们通常不需要在这里去访问这些文件的(都是通过 IDE 工具),所以我希望点击访达进来的时候就是 我的个人文件夹: 打开 访达Command + ,

从大学就开始折腾黑苹果,迟迟没有做些记录,总觉得差点什么,所以这次趁我需要重装系统的机会,拍了些照片,结合一些文字来做一个教程吧。 阅读按照文章顺序来,不要跳着来,除非我注明让你可以跳过。

OSX系统

不做评价谁好谁差,但是作为 Web工程师 的话,在 osx 系统下的编程体验确实要比 Windows 下好很多,软件也特别干净,从来没有在你工作的时候,突然右下角来个弹窗给你看广告,或者直接把广告就弹在最顶层来影响你的工作。 jdk npm git Dart Flutter 这些程序,在 unix 系的 osx 下也从来没有报错。所以还是推荐使用的。

安装须知

台式机

在我看来台式机安装黑果是比较完美的,能够屏蔽独立显卡,没有 WiFi 的顾虑。而且较高的性能使得能够更加完美的体验 osx 系统。 但是还是有些限制你需要知道:

  1. Intel 芯支持比较完美,如果是 AMD 芯的话,虽然说现在有破解,但是我还是觉得不如自然免驱的好(无测试过,没什么发言权);
  2. 一般比较热门的独立显卡,在国内远景论坛都有驱动的教程,涉及一些代码的注入,我比较懒,所以公司的电脑就拆掉了独显,使用集显来驱动,集显也是免驱的;

笔记本

  1. 笔记本受限比较大,现在笔记本一般都是 集显+独显 来自动切换达到省电的功能的,但是白苹果的笔记本,不是一个独显就是一个集显。所以,两者混合的情况下一般只能驱动集显(除非部分笔记本可以屏蔽集显),显卡问题就是独显基本就废了;
  2. WiFi 常年无解,两个方法解决:
    1. 淘宝购买 usb 接口的免驱网卡,卖家通常会提供使用所需要的驱动和软件;
    2. 更换笔记本内部的网卡,这一步比较专业,加上联想笔记本更换配件是需要刷白名单的,所以通常来说,都会选择第一种方式解决。

安装流程

镜像下载

因为在这篇文章产生的时候最新的 osx 系统是 10.15 catalina。所以我就是用这个系统来做教程吧。 osx_10.15_catalina 下载地址:下载地址

感谢远景论坛大佬的封装,我没有直接放大佬的最终下载地址,因为我还是希望各位能够给去下载,大佬有硬币赚。

镜像已经下好了,大佬也提供了常用的机型配置:

这一步只是先让你们看,先放一边,我们通常安装的时候不会直接选择合适的驱动,而是先使用通用驱动来安装,他是以最小的通用兼容配置来做的,通常都能够进入安装并且顺利通过。

写入优盘

此时,你需要拥有一个 16G 的优盘!记住记住,一定是 16G 不然他镜像可以刻录但是不会帮你刻录完整,不完整的镜像就是一个无法安装的镜像。(我用了一个 8G 的在这一步卡了好几个小时,最后把公司的师弟杀了祭天了,他出现了警告也不看直接点击 OK 就给我刻录了一个废盘) 首先你需要一个软件:TransMac 当然因为我的习惯,所以我都是在 Windows 系统下写优盘的。 安装,然后右键 -> 以管理员身份运行

会有提示需要购买正版什么的,不过点击 run 可以直接运行试用版,试用版就够用了。 然后找到你的优盘,右键选择 Format Disk –> Format with Disk Image 即可。

漫长的等待,结束了就可以使用这个优盘来安装系统了。

修改BIOS引导

msruefi,只能选择后者。一般新买的电脑或者近几年的电脑都是这个。 简单的识别方法:开机的时候,如果是品牌 Logo 然后就开始转圈圈进入 Windows 系统的话,就是 uefi。 第二就是修改 biossecurity boot,直接关闭就完事了。如果同等级的还有说什么引导什么系统的话,记得把它修改成 other

如果需要双系统

如果不需要双系统安装,直接跳过这一段

双系统指的是 Windows + osx,好像一般初学者都需要这个东西(包括之前的我自己)? 首先首先,Windows 默认的引导分区太小了,如果你不重新安装 Windows 的话,直接安装 osx 后面都会导致任何软件都无法安装。 教程 类似于上面地址里面的教程,但是在创建 efi 的时候,大小给大点,我一般给的是 512m,因为后面我们可能还需要根据不同的机型,在这个分区里面放入驱动。 分好区,先把 Windows 装上,记得留一些硬盘给 osx,我建议是 100G

装好系统后,在硬盘管理那里给 efi 分区随便给个盘符,然后把里面的东西备份一份出来,因为待会需要对他开刀,备份完整不至于你总是需要重新安装系统。

优盘引导开机

为了安装方便我建议将 first boot 设置为上面的优盘。当然使用 F12 来选择也行不过中途需要重启的时候就要留意重新选择了。 选择优盘中的带有 UEFI 选项的,这时候电脑将会读取优盘的引导,进入引导界面。

进入优盘是这个样子的:

一般是在第一项,下面会提示 Boot macOS Install from xxx 后面一般是优盘它默认给的名字。此时,我们 需要按一次 空格键 ,来到下一个页面:

下面是启动选项,一般按下空格需要设定的,大佬已经帮我们写好了,这个时候直接 按下回车键 进入安装软件就可以了。 一串命令后,我们会进来到这个页面:

能够来到这里,说明安装过程十分顺利,接下来进入下一个环节。

硬盘分区

就跟上面一样,我们选择磁盘工具,然后点击 继续,就会进入到磁盘工具的页面,那我们先选择显示所有设备,这时候磁盘都会显示出来。

然后我们选择整个硬盘(如果是单系统的话,双系统的话,你需要选择你要格式化的那个分区)点击上面的抹掉(看我鼠标那个位置):

点击之后会出来这个硬盘设定界面:

  1. 名称,可以自定义;

  2. 格式,新出来的文件系统是 APFS 听说是提速以及增加安全性的,那我就用这个好了,也建议使用这个。强烈不要使用区分大小写的分区格式(敲黑板),因为我使用了大小写区分的硬盘导致有些软件它不支持!!我目前就是因为这个原因重新安装系统的;

  3. 方案选择 GUID 分区图

稍稍等待一番,成功以后,直接左上角关闭磁盘管理工具。我们就可以安装系统了。

安装系统

回到这个界面,选择 安装 macOS


然后就同意协议了,这个不用图都知道要选择什么吧… 同意协议之后就是选择硬盘了,在这个界面,没有意外的话应该可以看到我们刚刚分出来的硬盘分区:

选好硬盘点击安装,接下来进入漫长的等待:

!!!请注意!!!几分钟动一次鼠标,不要让电脑进入睡眠,因为可能因为驱动原因会睡死。 然后系统在安装过程需要一次重启,别担心,如果你在之前已经设置好优盘第一启动了的话,就不用管它,没有的话,需要手动选择优盘引导,进入优盘不用做什么,会继续安装:

跑完基本就可以进入系统的基础设置页面了:

选择什么不用说了吧,选择其他国家也可以,只要看得懂就好…

完成安装

完成安装可以进入桌面了,左上角有个 -> 关于本机

哎哟卧槽,这个显存不对劲啊….. 那现在我们暂时忍一下,使用憋屈的动画效果使用一下 Safari,去下载一个软件:Clover Configurator 安装软件

LaunchPad 打开运行,不出意外的话:

LaunchPad/其他/终端 运行以下命令,输入设置的用户密码就可以了:

1
sudo spctl --master-disable

重新打开 Clover Configurator 来到这个菜单:

我们现在的引导还是优盘,所以先在优盘那一项点击后面的挂载,这时候应该需要输入用户的密码,挂载成功完以后,就可以在 访达 看到 EFI 分区了。

还记得这张图吗:

将原来的

config.plist 备份一份,把这里面的适合自己机型的配置,改名为 config.plist 放在上一层文件夹:

然后重启电脑,进入优盘引导,根据下面字样提示,选择 Boot macOS from 你设置的分区名 即可进入安装好的系统。如果配置兼容的话,重新查看关于本机:

显存正常了。 再尝试音乐,如果声卡也正常,那就完成安装了。

迁入硬盘引导

一切好了,总不能总是插着硬盘来开机吧,我们需要把硬盘的文件替换到磁盘里面去,还是使用 Clover Configurator

一个是硬盘的,一个是优盘的,同时挂载出来。 然后使用 访达 进行操作:

OK,bios 重新设置第一启动是硬盘,然后,如果需要进入 osx 则选择 osx 硬盘回车,需要 Windows 的话就选择 Windows 所在的分区即可。

其他设置

这是我自己觉得比较好的设置,每个人可以按照自己的习惯来。 当然关于原来在 Windows 习惯的 Ctrl + c 这些都需要换成 Command + C 了,Command 在原来的 Win键 上面,我是不建议在系统设置里面替换这两者的按键位置的,毕竟用起来还蛮舒服的。 然后所有软件需要进入设置界面,都已经统一是 Command + , 这个快捷键了。

访达小设置

访达默认设置我感觉是不太好用的,比如点击访达,显示的不是我的电脑的内容,而是最近使用的内容,我特么写代码最近使用的内容都是代码文件,而我们通常不需要在这里去访问这些文件的(都是通过 IDE 工具),所以我希望点击访达进来的时候就是 我的个人文件夹: 打开 访达Command + ,

第二个不好的点是,我去到那个文件夹,要搜索某个文件,默认给我跑整个电脑去搜索,速度慢又没必要,所以:

第三个不好的点是,文件默认居然不排序,还要像垃圾堆一样堆在一起!!改掉设置!: 这个不在 Command + , 里边了,在这里:

开启Retina渲染

参考资料: 黑苹果开启缩放分辨率HiDPi以及字体模糊的调整方法总结 黑苹果开启HiDPI“解决”字体模糊的问题 直接在 其他软件/终端 粘贴下面的命令运行,提示输入用户密码,输入完以后根据提示来走就可以了:

1
sh -c "$(curl -fsSL https://raw.githubusercontent.com/xzhih/one-key-hidpi/master/hidpi.sh)"

重启电脑后安装 RDM 工具来配置分辨率(下载 RDM-2.2.dmg 就可以了 ) 运行,右上角:

选择带有小闪电的就是 HiDPI 的设置(也就是 Retina 效果)

一般根据屏幕原始分辨率来定,有 4K 显示器开 2K 分辨率的话,效果就跟白苹果一样了,2倍的差距,我目前原始分辨率是 1920 * 1080,开图中的分辨率也就凑合凑合过吧

然后在 系统偏好设置 –> 通用 里面开启字体平滑(在最下面,默认也是开启的)

我对默认的平滑程度还是不太喜欢,要开最高的,终端运行以下命令,提示输入用户密码,输入完以后重启即可:

1
defaults -currentHost write -globalDomain AppleFontSmoothing -int 3

来看看效果图: 开启前: 开启后:

结束

后面有空再更更常用软件了。

第二个不好的点是,我去到那个文件夹,要搜索某个文件,默认给我跑整个电脑去搜索,速度慢又没必要,所以:

从大学就开始折腾黑苹果,迟迟没有做些记录,总觉得差点什么,所以这次趁我需要重装系统的机会,拍了些照片,结合一些文字来做一个教程吧。 阅读按照文章顺序来,不要跳着来,除非我注明让你可以跳过。

OSX系统

不做评价谁好谁差,但是作为 Web工程师 的话,在 osx 系统下的编程体验确实要比 Windows 下好很多,软件也特别干净,从来没有在你工作的时候,突然右下角来个弹窗给你看广告,或者直接把广告就弹在最顶层来影响你的工作。 jdk npm git Dart Flutter 这些程序,在 unix 系的 osx 下也从来没有报错。所以还是推荐使用的。

安装须知

台式机

在我看来台式机安装黑果是比较完美的,能够屏蔽独立显卡,没有 WiFi 的顾虑。而且较高的性能使得能够更加完美的体验 osx 系统。 但是还是有些限制你需要知道:

  1. Intel 芯支持比较完美,如果是 AMD 芯的话,虽然说现在有破解,但是我还是觉得不如自然免驱的好(无测试过,没什么发言权);
  2. 一般比较热门的独立显卡,在国内远景论坛都有驱动的教程,涉及一些代码的注入,我比较懒,所以公司的电脑就拆掉了独显,使用集显来驱动,集显也是免驱的;

笔记本

  1. 笔记本受限比较大,现在笔记本一般都是 集显+独显 来自动切换达到省电的功能的,但是白苹果的笔记本,不是一个独显就是一个集显。所以,两者混合的情况下一般只能驱动集显(除非部分笔记本可以屏蔽集显),显卡问题就是独显基本就废了;
  2. WiFi 常年无解,两个方法解决:
    1. 淘宝购买 usb 接口的免驱网卡,卖家通常会提供使用所需要的驱动和软件;
    2. 更换笔记本内部的网卡,这一步比较专业,加上联想笔记本更换配件是需要刷白名单的,所以通常来说,都会选择第一种方式解决。

安装流程

镜像下载

因为在这篇文章产生的时候最新的 osx 系统是 10.15 catalina。所以我就是用这个系统来做教程吧。 osx_10.15_catalina 下载地址:下载地址

感谢远景论坛大佬的封装,我没有直接放大佬的最终下载地址,因为我还是希望各位能够给去下载,大佬有硬币赚。

镜像已经下好了,大佬也提供了常用的机型配置:

这一步只是先让你们看,先放一边,我们通常安装的时候不会直接选择合适的驱动,而是先使用通用驱动来安装,他是以最小的通用兼容配置来做的,通常都能够进入安装并且顺利通过。

写入优盘

此时,你需要拥有一个 16G 的优盘!记住记住,一定是 16G 不然他镜像可以刻录但是不会帮你刻录完整,不完整的镜像就是一个无法安装的镜像。(我用了一个 8G 的在这一步卡了好几个小时,最后把公司的师弟杀了祭天了,他出现了警告也不看直接点击 OK 就给我刻录了一个废盘) 首先你需要一个软件:TransMac 当然因为我的习惯,所以我都是在 Windows 系统下写优盘的。 安装,然后右键 -> 以管理员身份运行

会有提示需要购买正版什么的,不过点击 run 可以直接运行试用版,试用版就够用了。 然后找到你的优盘,右键选择 Format Disk –> Format with Disk Image 即可。

漫长的等待,结束了就可以使用这个优盘来安装系统了。

修改BIOS引导

msruefi,只能选择后者。一般新买的电脑或者近几年的电脑都是这个。 简单的识别方法:开机的时候,如果是品牌 Logo 然后就开始转圈圈进入 Windows 系统的话,就是 uefi。 第二就是修改 biossecurity boot,直接关闭就完事了。如果同等级的还有说什么引导什么系统的话,记得把它修改成 other

如果需要双系统

如果不需要双系统安装,直接跳过这一段

双系统指的是 Windows + osx,好像一般初学者都需要这个东西(包括之前的我自己)? 首先首先,Windows 默认的引导分区太小了,如果你不重新安装 Windows 的话,直接安装 osx 后面都会导致任何软件都无法安装。 教程 类似于上面地址里面的教程,但是在创建 efi 的时候,大小给大点,我一般给的是 512m,因为后面我们可能还需要根据不同的机型,在这个分区里面放入驱动。 分好区,先把 Windows 装上,记得留一些硬盘给 osx,我建议是 100G

装好系统后,在硬盘管理那里给 efi 分区随便给个盘符,然后把里面的东西备份一份出来,因为待会需要对他开刀,备份完整不至于你总是需要重新安装系统。

优盘引导开机

为了安装方便我建议将 first boot 设置为上面的优盘。当然使用 F12 来选择也行不过中途需要重启的时候就要留意重新选择了。 选择优盘中的带有 UEFI 选项的,这时候电脑将会读取优盘的引导,进入引导界面。

进入优盘是这个样子的:

一般是在第一项,下面会提示 Boot macOS Install from xxx 后面一般是优盘它默认给的名字。此时,我们 需要按一次 空格键 ,来到下一个页面:

下面是启动选项,一般按下空格需要设定的,大佬已经帮我们写好了,这个时候直接 按下回车键 进入安装软件就可以了。 一串命令后,我们会进来到这个页面:

能够来到这里,说明安装过程十分顺利,接下来进入下一个环节。

硬盘分区

就跟上面一样,我们选择磁盘工具,然后点击 继续,就会进入到磁盘工具的页面,那我们先选择显示所有设备,这时候磁盘都会显示出来。

然后我们选择整个硬盘(如果是单系统的话,双系统的话,你需要选择你要格式化的那个分区)点击上面的抹掉(看我鼠标那个位置):

点击之后会出来这个硬盘设定界面:

  1. 名称,可以自定义;

  2. 格式,新出来的文件系统是 APFS 听说是提速以及增加安全性的,那我就用这个好了,也建议使用这个。强烈不要使用区分大小写的分区格式(敲黑板),因为我使用了大小写区分的硬盘导致有些软件它不支持!!我目前就是因为这个原因重新安装系统的;

  3. 方案选择 GUID 分区图

稍稍等待一番,成功以后,直接左上角关闭磁盘管理工具。我们就可以安装系统了。

安装系统

回到这个界面,选择 安装 macOS


然后就同意协议了,这个不用图都知道要选择什么吧… 同意协议之后就是选择硬盘了,在这个界面,没有意外的话应该可以看到我们刚刚分出来的硬盘分区:

选好硬盘点击安装,接下来进入漫长的等待:

!!!请注意!!!几分钟动一次鼠标,不要让电脑进入睡眠,因为可能因为驱动原因会睡死。 然后系统在安装过程需要一次重启,别担心,如果你在之前已经设置好优盘第一启动了的话,就不用管它,没有的话,需要手动选择优盘引导,进入优盘不用做什么,会继续安装:

跑完基本就可以进入系统的基础设置页面了:

选择什么不用说了吧,选择其他国家也可以,只要看得懂就好…

完成安装

完成安装可以进入桌面了,左上角有个 -> 关于本机

哎哟卧槽,这个显存不对劲啊….. 那现在我们暂时忍一下,使用憋屈的动画效果使用一下 Safari,去下载一个软件:Clover Configurator 安装软件

LaunchPad 打开运行,不出意外的话:

LaunchPad/其他/终端 运行以下命令,输入设置的用户密码就可以了:

1
sudo spctl --master-disable

重新打开 Clover Configurator 来到这个菜单:

我们现在的引导还是优盘,所以先在优盘那一项点击后面的挂载,这时候应该需要输入用户的密码,挂载成功完以后,就可以在 访达 看到 EFI 分区了。

还记得这张图吗:

将原来的

config.plist 备份一份,把这里面的适合自己机型的配置,改名为 config.plist 放在上一层文件夹:

然后重启电脑,进入优盘引导,根据下面字样提示,选择 Boot macOS from 你设置的分区名 即可进入安装好的系统。如果配置兼容的话,重新查看关于本机:

显存正常了。 再尝试音乐,如果声卡也正常,那就完成安装了。

迁入硬盘引导

一切好了,总不能总是插着硬盘来开机吧,我们需要把硬盘的文件替换到磁盘里面去,还是使用 Clover Configurator

一个是硬盘的,一个是优盘的,同时挂载出来。 然后使用 访达 进行操作:

OK,bios 重新设置第一启动是硬盘,然后,如果需要进入 osx 则选择 osx 硬盘回车,需要 Windows 的话就选择 Windows 所在的分区即可。

其他设置

这是我自己觉得比较好的设置,每个人可以按照自己的习惯来。 当然关于原来在 Windows 习惯的 Ctrl + c 这些都需要换成 Command + C 了,Command 在原来的 Win键 上面,我是不建议在系统设置里面替换这两者的按键位置的,毕竟用起来还蛮舒服的。 然后所有软件需要进入设置界面,都已经统一是 Command + , 这个快捷键了。

访达小设置

访达默认设置我感觉是不太好用的,比如点击访达,显示的不是我的电脑的内容,而是最近使用的内容,我特么写代码最近使用的内容都是代码文件,而我们通常不需要在这里去访问这些文件的(都是通过 IDE 工具),所以我希望点击访达进来的时候就是 我的个人文件夹: 打开 访达Command + ,

第二个不好的点是,我去到那个文件夹,要搜索某个文件,默认给我跑整个电脑去搜索,速度慢又没必要,所以:

第三个不好的点是,文件默认居然不排序,还要像垃圾堆一样堆在一起!!改掉设置!: 这个不在 Command + , 里边了,在这里:

开启Retina渲染

参考资料: 黑苹果开启缩放分辨率HiDPi以及字体模糊的调整方法总结 黑苹果开启HiDPI“解决”字体模糊的问题 直接在 其他软件/终端 粘贴下面的命令运行,提示输入用户密码,输入完以后根据提示来走就可以了:

1
sh -c "$(curl -fsSL https://raw.githubusercontent.com/xzhih/one-key-hidpi/master/hidpi.sh)"

重启电脑后安装 RDM 工具来配置分辨率(下载 RDM-2.2.dmg 就可以了 ) 运行,右上角:

选择带有小闪电的就是 HiDPI 的设置(也就是 Retina 效果)

一般根据屏幕原始分辨率来定,有 4K 显示器开 2K 分辨率的话,效果就跟白苹果一样了,2倍的差距,我目前原始分辨率是 1920 * 1080,开图中的分辨率也就凑合凑合过吧

然后在 系统偏好设置 –> 通用 里面开启字体平滑(在最下面,默认也是开启的)

我对默认的平滑程度还是不太喜欢,要开最高的,终端运行以下命令,提示输入用户密码,输入完以后重启即可:

1
defaults -currentHost write -globalDomain AppleFontSmoothing -int 3

来看看效果图: 开启前: 开启后:

结束

后面有空再更更常用软件了。

第三个不好的点是,文件默认居然不排序,还要像垃圾堆一样堆在一起!!改掉设置!: 这个不在 Command + , 里边了,在这里:

从大学就开始折腾黑苹果,迟迟没有做些记录,总觉得差点什么,所以这次趁我需要重装系统的机会,拍了些照片,结合一些文字来做一个教程吧。 阅读按照文章顺序来,不要跳着来,除非我注明让你可以跳过。

OSX系统

不做评价谁好谁差,但是作为 Web工程师 的话,在 osx 系统下的编程体验确实要比 Windows 下好很多,软件也特别干净,从来没有在你工作的时候,突然右下角来个弹窗给你看广告,或者直接把广告就弹在最顶层来影响你的工作。 jdk npm git Dart Flutter 这些程序,在 unix 系的 osx 下也从来没有报错。所以还是推荐使用的。

安装须知

台式机

在我看来台式机安装黑果是比较完美的,能够屏蔽独立显卡,没有 WiFi 的顾虑。而且较高的性能使得能够更加完美的体验 osx 系统。 但是还是有些限制你需要知道:

  1. Intel 芯支持比较完美,如果是 AMD 芯的话,虽然说现在有破解,但是我还是觉得不如自然免驱的好(无测试过,没什么发言权);
  2. 一般比较热门的独立显卡,在国内远景论坛都有驱动的教程,涉及一些代码的注入,我比较懒,所以公司的电脑就拆掉了独显,使用集显来驱动,集显也是免驱的;

笔记本

  1. 笔记本受限比较大,现在笔记本一般都是 集显+独显 来自动切换达到省电的功能的,但是白苹果的笔记本,不是一个独显就是一个集显。所以,两者混合的情况下一般只能驱动集显(除非部分笔记本可以屏蔽集显),显卡问题就是独显基本就废了;
  2. WiFi 常年无解,两个方法解决:
    1. 淘宝购买 usb 接口的免驱网卡,卖家通常会提供使用所需要的驱动和软件;
    2. 更换笔记本内部的网卡,这一步比较专业,加上联想笔记本更换配件是需要刷白名单的,所以通常来说,都会选择第一种方式解决。

安装流程

镜像下载

因为在这篇文章产生的时候最新的 osx 系统是 10.15 catalina。所以我就是用这个系统来做教程吧。 osx_10.15_catalina 下载地址:下载地址

感谢远景论坛大佬的封装,我没有直接放大佬的最终下载地址,因为我还是希望各位能够给去下载,大佬有硬币赚。

镜像已经下好了,大佬也提供了常用的机型配置:

这一步只是先让你们看,先放一边,我们通常安装的时候不会直接选择合适的驱动,而是先使用通用驱动来安装,他是以最小的通用兼容配置来做的,通常都能够进入安装并且顺利通过。

写入优盘

此时,你需要拥有一个 16G 的优盘!记住记住,一定是 16G 不然他镜像可以刻录但是不会帮你刻录完整,不完整的镜像就是一个无法安装的镜像。(我用了一个 8G 的在这一步卡了好几个小时,最后把公司的师弟杀了祭天了,他出现了警告也不看直接点击 OK 就给我刻录了一个废盘) 首先你需要一个软件:TransMac 当然因为我的习惯,所以我都是在 Windows 系统下写优盘的。 安装,然后右键 -> 以管理员身份运行

会有提示需要购买正版什么的,不过点击 run 可以直接运行试用版,试用版就够用了。 然后找到你的优盘,右键选择 Format Disk –> Format with Disk Image 即可。

漫长的等待,结束了就可以使用这个优盘来安装系统了。

修改BIOS引导

msruefi,只能选择后者。一般新买的电脑或者近几年的电脑都是这个。 简单的识别方法:开机的时候,如果是品牌 Logo 然后就开始转圈圈进入 Windows 系统的话,就是 uefi。 第二就是修改 biossecurity boot,直接关闭就完事了。如果同等级的还有说什么引导什么系统的话,记得把它修改成 other

如果需要双系统

如果不需要双系统安装,直接跳过这一段

双系统指的是 Windows + osx,好像一般初学者都需要这个东西(包括之前的我自己)? 首先首先,Windows 默认的引导分区太小了,如果你不重新安装 Windows 的话,直接安装 osx 后面都会导致任何软件都无法安装。 教程 类似于上面地址里面的教程,但是在创建 efi 的时候,大小给大点,我一般给的是 512m,因为后面我们可能还需要根据不同的机型,在这个分区里面放入驱动。 分好区,先把 Windows 装上,记得留一些硬盘给 osx,我建议是 100G

装好系统后,在硬盘管理那里给 efi 分区随便给个盘符,然后把里面的东西备份一份出来,因为待会需要对他开刀,备份完整不至于你总是需要重新安装系统。

优盘引导开机

为了安装方便我建议将 first boot 设置为上面的优盘。当然使用 F12 来选择也行不过中途需要重启的时候就要留意重新选择了。 选择优盘中的带有 UEFI 选项的,这时候电脑将会读取优盘的引导,进入引导界面。

进入优盘是这个样子的:

一般是在第一项,下面会提示 Boot macOS Install from xxx 后面一般是优盘它默认给的名字。此时,我们 需要按一次 空格键 ,来到下一个页面:

下面是启动选项,一般按下空格需要设定的,大佬已经帮我们写好了,这个时候直接 按下回车键 进入安装软件就可以了。 一串命令后,我们会进来到这个页面:

能够来到这里,说明安装过程十分顺利,接下来进入下一个环节。

硬盘分区

就跟上面一样,我们选择磁盘工具,然后点击 继续,就会进入到磁盘工具的页面,那我们先选择显示所有设备,这时候磁盘都会显示出来。

然后我们选择整个硬盘(如果是单系统的话,双系统的话,你需要选择你要格式化的那个分区)点击上面的抹掉(看我鼠标那个位置):

点击之后会出来这个硬盘设定界面:

  1. 名称,可以自定义;

  2. 格式,新出来的文件系统是 APFS 听说是提速以及增加安全性的,那我就用这个好了,也建议使用这个。强烈不要使用区分大小写的分区格式(敲黑板),因为我使用了大小写区分的硬盘导致有些软件它不支持!!我目前就是因为这个原因重新安装系统的;

  3. 方案选择 GUID 分区图

稍稍等待一番,成功以后,直接左上角关闭磁盘管理工具。我们就可以安装系统了。

安装系统

回到这个界面,选择 安装 macOS


然后就同意协议了,这个不用图都知道要选择什么吧… 同意协议之后就是选择硬盘了,在这个界面,没有意外的话应该可以看到我们刚刚分出来的硬盘分区:

选好硬盘点击安装,接下来进入漫长的等待:

!!!请注意!!!几分钟动一次鼠标,不要让电脑进入睡眠,因为可能因为驱动原因会睡死。 然后系统在安装过程需要一次重启,别担心,如果你在之前已经设置好优盘第一启动了的话,就不用管它,没有的话,需要手动选择优盘引导,进入优盘不用做什么,会继续安装:

跑完基本就可以进入系统的基础设置页面了:

选择什么不用说了吧,选择其他国家也可以,只要看得懂就好…

完成安装

完成安装可以进入桌面了,左上角有个 -> 关于本机

哎哟卧槽,这个显存不对劲啊….. 那现在我们暂时忍一下,使用憋屈的动画效果使用一下 Safari,去下载一个软件:Clover Configurator 安装软件

LaunchPad 打开运行,不出意外的话:

LaunchPad/其他/终端 运行以下命令,输入设置的用户密码就可以了:

1
sudo spctl --master-disable

重新打开 Clover Configurator 来到这个菜单:

我们现在的引导还是优盘,所以先在优盘那一项点击后面的挂载,这时候应该需要输入用户的密码,挂载成功完以后,就可以在 访达 看到 EFI 分区了。

还记得这张图吗:

将原来的

config.plist 备份一份,把这里面的适合自己机型的配置,改名为 config.plist 放在上一层文件夹:

然后重启电脑,进入优盘引导,根据下面字样提示,选择 Boot macOS from 你设置的分区名 即可进入安装好的系统。如果配置兼容的话,重新查看关于本机:

显存正常了。 再尝试音乐,如果声卡也正常,那就完成安装了。

迁入硬盘引导

一切好了,总不能总是插着硬盘来开机吧,我们需要把硬盘的文件替换到磁盘里面去,还是使用 Clover Configurator

一个是硬盘的,一个是优盘的,同时挂载出来。 然后使用 访达 进行操作:

OK,bios 重新设置第一启动是硬盘,然后,如果需要进入 osx 则选择 osx 硬盘回车,需要 Windows 的话就选择 Windows 所在的分区即可。

其他设置

这是我自己觉得比较好的设置,每个人可以按照自己的习惯来。 当然关于原来在 Windows 习惯的 Ctrl + c 这些都需要换成 Command + C 了,Command 在原来的 Win键 上面,我是不建议在系统设置里面替换这两者的按键位置的,毕竟用起来还蛮舒服的。 然后所有软件需要进入设置界面,都已经统一是 Command + , 这个快捷键了。

访达小设置

访达默认设置我感觉是不太好用的,比如点击访达,显示的不是我的电脑的内容,而是最近使用的内容,我特么写代码最近使用的内容都是代码文件,而我们通常不需要在这里去访问这些文件的(都是通过 IDE 工具),所以我希望点击访达进来的时候就是 我的个人文件夹: 打开 访达Command + ,

第二个不好的点是,我去到那个文件夹,要搜索某个文件,默认给我跑整个电脑去搜索,速度慢又没必要,所以:

第三个不好的点是,文件默认居然不排序,还要像垃圾堆一样堆在一起!!改掉设置!: 这个不在 Command + , 里边了,在这里:

开启Retina渲染

参考资料: 黑苹果开启缩放分辨率HiDPi以及字体模糊的调整方法总结 黑苹果开启HiDPI“解决”字体模糊的问题 直接在 其他软件/终端 粘贴下面的命令运行,提示输入用户密码,输入完以后根据提示来走就可以了:

1
sh -c "$(curl -fsSL https://raw.githubusercontent.com/xzhih/one-key-hidpi/master/hidpi.sh)"

重启电脑后安装 RDM 工具来配置分辨率(下载 RDM-2.2.dmg 就可以了 ) 运行,右上角:

选择带有小闪电的就是 HiDPI 的设置(也就是 Retina 效果)

一般根据屏幕原始分辨率来定,有 4K 显示器开 2K 分辨率的话,效果就跟白苹果一样了,2倍的差距,我目前原始分辨率是 1920 * 1080,开图中的分辨率也就凑合凑合过吧

然后在 系统偏好设置 –> 通用 里面开启字体平滑(在最下面,默认也是开启的)

我对默认的平滑程度还是不太喜欢,要开最高的,终端运行以下命令,提示输入用户密码,输入完以后重启即可:

1
defaults -currentHost write -globalDomain AppleFontSmoothing -int 3

来看看效果图: 开启前: 开启后:

结束

后面有空再更更常用软件了。

从大学就开始折腾黑苹果,迟迟没有做些记录,总觉得差点什么,所以这次趁我需要重装系统的机会,拍了些照片,结合一些文字来做一个教程吧。 阅读按照文章顺序来,不要跳着来,除非我注明让你可以跳过。

OSX系统

不做评价谁好谁差,但是作为 Web工程师 的话,在 osx 系统下的编程体验确实要比 Windows 下好很多,软件也特别干净,从来没有在你工作的时候,突然右下角来个弹窗给你看广告,或者直接把广告就弹在最顶层来影响你的工作。 jdk npm git Dart Flutter 这些程序,在 unix 系的 osx 下也从来没有报错。所以还是推荐使用的。

安装须知

台式机

在我看来台式机安装黑果是比较完美的,能够屏蔽独立显卡,没有 WiFi 的顾虑。而且较高的性能使得能够更加完美的体验 osx 系统。 但是还是有些限制你需要知道:

  1. Intel 芯支持比较完美,如果是 AMD 芯的话,虽然说现在有破解,但是我还是觉得不如自然免驱的好(无测试过,没什么发言权);
  2. 一般比较热门的独立显卡,在国内远景论坛都有驱动的教程,涉及一些代码的注入,我比较懒,所以公司的电脑就拆掉了独显,使用集显来驱动,集显也是免驱的;

笔记本

  1. 笔记本受限比较大,现在笔记本一般都是 集显+独显 来自动切换达到省电的功能的,但是白苹果的笔记本,不是一个独显就是一个集显。所以,两者混合的情况下一般只能驱动集显(除非部分笔记本可以屏蔽集显),显卡问题就是独显基本就废了;
  2. WiFi 常年无解,两个方法解决:
    1. 淘宝购买 usb 接口的免驱网卡,卖家通常会提供使用所需要的驱动和软件;
    2. 更换笔记本内部的网卡,这一步比较专业,加上联想笔记本更换配件是需要刷白名单的,所以通常来说,都会选择第一种方式解决。

安装流程

镜像下载

因为在这篇文章产生的时候最新的 osx 系统是 10.15 catalina。所以我就是用这个系统来做教程吧。 osx_10.15_catalina 下载地址:下载地址

感谢远景论坛大佬的封装,我没有直接放大佬的最终下载地址,因为我还是希望各位能够给去下载,大佬有硬币赚。

镜像已经下好了,大佬也提供了常用的机型配置:

这一步只是先让你们看,先放一边,我们通常安装的时候不会直接选择合适的驱动,而是先使用通用驱动来安装,他是以最小的通用兼容配置来做的,通常都能够进入安装并且顺利通过。

写入优盘

此时,你需要拥有一个 16G 的优盘!记住记住,一定是 16G 不然他镜像可以刻录但是不会帮你刻录完整,不完整的镜像就是一个无法安装的镜像。(我用了一个 8G 的在这一步卡了好几个小时,最后把公司的师弟杀了祭天了,他出现了警告也不看直接点击 OK 就给我刻录了一个废盘) 首先你需要一个软件:TransMac 当然因为我的习惯,所以我都是在 Windows 系统下写优盘的。 安装,然后右键 -> 以管理员身份运行

会有提示需要购买正版什么的,不过点击 run 可以直接运行试用版,试用版就够用了。 然后找到你的优盘,右键选择 Format Disk –> Format with Disk Image 即可。

漫长的等待,结束了就可以使用这个优盘来安装系统了。

修改BIOS引导

msruefi,只能选择后者。一般新买的电脑或者近几年的电脑都是这个。 简单的识别方法:开机的时候,如果是品牌 Logo 然后就开始转圈圈进入 Windows 系统的话,就是 uefi。 第二就是修改 biossecurity boot,直接关闭就完事了。如果同等级的还有说什么引导什么系统的话,记得把它修改成 other

如果需要双系统

如果不需要双系统安装,直接跳过这一段

双系统指的是 Windows + osx,好像一般初学者都需要这个东西(包括之前的我自己)? 首先首先,Windows 默认的引导分区太小了,如果你不重新安装 Windows 的话,直接安装 osx 后面都会导致任何软件都无法安装。 教程 类似于上面地址里面的教程,但是在创建 efi 的时候,大小给大点,我一般给的是 512m,因为后面我们可能还需要根据不同的机型,在这个分区里面放入驱动。 分好区,先把 Windows 装上,记得留一些硬盘给 osx,我建议是 100G

装好系统后,在硬盘管理那里给 efi 分区随便给个盘符,然后把里面的东西备份一份出来,因为待会需要对他开刀,备份完整不至于你总是需要重新安装系统。

优盘引导开机

为了安装方便我建议将 first boot 设置为上面的优盘。当然使用 F12 来选择也行不过中途需要重启的时候就要留意重新选择了。 选择优盘中的带有 UEFI 选项的,这时候电脑将会读取优盘的引导,进入引导界面。

进入优盘是这个样子的:

一般是在第一项,下面会提示 Boot macOS Install from xxx 后面一般是优盘它默认给的名字。此时,我们 需要按一次 空格键 ,来到下一个页面:

下面是启动选项,一般按下空格需要设定的,大佬已经帮我们写好了,这个时候直接 按下回车键 进入安装软件就可以了。 一串命令后,我们会进来到这个页面:

能够来到这里,说明安装过程十分顺利,接下来进入下一个环节。

硬盘分区

就跟上面一样,我们选择磁盘工具,然后点击 继续,就会进入到磁盘工具的页面,那我们先选择显示所有设备,这时候磁盘都会显示出来。

然后我们选择整个硬盘(如果是单系统的话,双系统的话,你需要选择你要格式化的那个分区)点击上面的抹掉(看我鼠标那个位置):

点击之后会出来这个硬盘设定界面:

  1. 名称,可以自定义;

  2. 格式,新出来的文件系统是 APFS 听说是提速以及增加安全性的,那我就用这个好了,也建议使用这个。强烈不要使用区分大小写的分区格式(敲黑板),因为我使用了大小写区分的硬盘导致有些软件它不支持!!我目前就是因为这个原因重新安装系统的;

  3. 方案选择 GUID 分区图

稍稍等待一番,成功以后,直接左上角关闭磁盘管理工具。我们就可以安装系统了。

安装系统

回到这个界面,选择 安装 macOS


然后就同意协议了,这个不用图都知道要选择什么吧… 同意协议之后就是选择硬盘了,在这个界面,没有意外的话应该可以看到我们刚刚分出来的硬盘分区:

选好硬盘点击安装,接下来进入漫长的等待:

!!!请注意!!!几分钟动一次鼠标,不要让电脑进入睡眠,因为可能因为驱动原因会睡死。 然后系统在安装过程需要一次重启,别担心,如果你在之前已经设置好优盘第一启动了的话,就不用管它,没有的话,需要手动选择优盘引导,进入优盘不用做什么,会继续安装:

跑完基本就可以进入系统的基础设置页面了:

选择什么不用说了吧,选择其他国家也可以,只要看得懂就好…

完成安装

完成安装可以进入桌面了,左上角有个 -> 关于本机

哎哟卧槽,这个显存不对劲啊….. 那现在我们暂时忍一下,使用憋屈的动画效果使用一下 Safari,去下载一个软件:Clover Configurator 安装软件

LaunchPad 打开运行,不出意外的话:

LaunchPad/其他/终端 运行以下命令,输入设置的用户密码就可以了:

1
sudo spctl --master-disable

重新打开 Clover Configurator 来到这个菜单:

我们现在的引导还是优盘,所以先在优盘那一项点击后面的挂载,这时候应该需要输入用户的密码,挂载成功完以后,就可以在 访达 看到 EFI 分区了。

还记得这张图吗:

将原来的

config.plist 备份一份,把这里面的适合自己机型的配置,改名为 config.plist 放在上一层文件夹:

然后重启电脑,进入优盘引导,根据下面字样提示,选择 Boot macOS from 你设置的分区名 即可进入安装好的系统。如果配置兼容的话,重新查看关于本机:

显存正常了。 再尝试音乐,如果声卡也正常,那就完成安装了。

迁入硬盘引导

一切好了,总不能总是插着硬盘来开机吧,我们需要把硬盘的文件替换到磁盘里面去,还是使用 Clover Configurator

一个是硬盘的,一个是优盘的,同时挂载出来。 然后使用 访达 进行操作:

OK,bios 重新设置第一启动是硬盘,然后,如果需要进入 osx 则选择 osx 硬盘回车,需要 Windows 的话就选择 Windows 所在的分区即可。

其他设置

这是我自己觉得比较好的设置,每个人可以按照自己的习惯来。 当然关于原来在 Windows 习惯的 Ctrl + c 这些都需要换成 Command + C 了,Command 在原来的 Win键 上面,我是不建议在系统设置里面替换这两者的按键位置的,毕竟用起来还蛮舒服的。 然后所有软件需要进入设置界面,都已经统一是 Command + , 这个快捷键了。

访达小设置

访达默认设置我感觉是不太好用的,比如点击访达,显示的不是我的电脑的内容,而是最近使用的内容,我特么写代码最近使用的内容都是代码文件,而我们通常不需要在这里去访问这些文件的(都是通过 IDE 工具),所以我希望点击访达进来的时候就是 我的个人文件夹: 打开 访达Command + ,

第二个不好的点是,我去到那个文件夹,要搜索某个文件,默认给我跑整个电脑去搜索,速度慢又没必要,所以:

第三个不好的点是,文件默认居然不排序,还要像垃圾堆一样堆在一起!!改掉设置!: 这个不在 Command + , 里边了,在这里:

开启Retina渲染

参考资料: 黑苹果开启缩放分辨率HiDPi以及字体模糊的调整方法总结 黑苹果开启HiDPI“解决”字体模糊的问题 直接在 其他软件/终端 粘贴下面的命令运行,提示输入用户密码,输入完以后根据提示来走就可以了:

1
sh -c "$(curl -fsSL https://raw.githubusercontent.com/xzhih/one-key-hidpi/master/hidpi.sh)"

重启电脑后安装 RDM 工具来配置分辨率(下载 RDM-2.2.dmg 就可以了 ) 运行,右上角:

选择带有小闪电的就是 HiDPI 的设置(也就是 Retina 效果)

一般根据屏幕原始分辨率来定,有 4K 显示器开 2K 分辨率的话,效果就跟白苹果一样了,2倍的差距,我目前原始分辨率是 1920 * 1080,开图中的分辨率也就凑合凑合过吧

然后在 系统偏好设置 –> 通用 里面开启字体平滑(在最下面,默认也是开启的)

我对默认的平滑程度还是不太喜欢,要开最高的,终端运行以下命令,提示输入用户密码,输入完以后重启即可:

1
defaults -currentHost write -globalDomain AppleFontSmoothing -int 3

来看看效果图: 开启前: 开启后:

结束

后面有空再更更常用软件了。

开启Retina渲染

参考资料: 黑苹果开启缩放分辨率HiDPi以及字体模糊的调整方法总结 黑苹果开启HiDPI“解决”字体模糊的问题 直接在 其他软件/终端 粘贴下面的命令运行,提示输入用户密码,输入完以后根据提示来走就可以了:

1
sh -c "$(curl -fsSL https://raw.githubusercontent.com/xzhih/one-key-hidpi/master/hidpi.sh)"

重启电脑后安装 RDM 工具来配置分辨率(下载 RDM-2.2.dmg 就可以了 ) 运行,右上角:

从大学就开始折腾黑苹果,迟迟没有做些记录,总觉得差点什么,所以这次趁我需要重装系统的机会,拍了些照片,结合一些文字来做一个教程吧。 阅读按照文章顺序来,不要跳着来,除非我注明让你可以跳过。

OSX系统

不做评价谁好谁差,但是作为 Web工程师 的话,在 osx 系统下的编程体验确实要比 Windows 下好很多,软件也特别干净,从来没有在你工作的时候,突然右下角来个弹窗给你看广告,或者直接把广告就弹在最顶层来影响你的工作。 jdk npm git Dart Flutter 这些程序,在 unix 系的 osx 下也从来没有报错。所以还是推荐使用的。

安装须知

台式机

在我看来台式机安装黑果是比较完美的,能够屏蔽独立显卡,没有 WiFi 的顾虑。而且较高的性能使得能够更加完美的体验 osx 系统。 但是还是有些限制你需要知道:

  1. Intel 芯支持比较完美,如果是 AMD 芯的话,虽然说现在有破解,但是我还是觉得不如自然免驱的好(无测试过,没什么发言权);
  2. 一般比较热门的独立显卡,在国内远景论坛都有驱动的教程,涉及一些代码的注入,我比较懒,所以公司的电脑就拆掉了独显,使用集显来驱动,集显也是免驱的;

笔记本

  1. 笔记本受限比较大,现在笔记本一般都是 集显+独显 来自动切换达到省电的功能的,但是白苹果的笔记本,不是一个独显就是一个集显。所以,两者混合的情况下一般只能驱动集显(除非部分笔记本可以屏蔽集显),显卡问题就是独显基本就废了;
  2. WiFi 常年无解,两个方法解决:
    1. 淘宝购买 usb 接口的免驱网卡,卖家通常会提供使用所需要的驱动和软件;
    2. 更换笔记本内部的网卡,这一步比较专业,加上联想笔记本更换配件是需要刷白名单的,所以通常来说,都会选择第一种方式解决。

安装流程

镜像下载

因为在这篇文章产生的时候最新的 osx 系统是 10.15 catalina。所以我就是用这个系统来做教程吧。 osx_10.15_catalina 下载地址:下载地址

感谢远景论坛大佬的封装,我没有直接放大佬的最终下载地址,因为我还是希望各位能够给去下载,大佬有硬币赚。

镜像已经下好了,大佬也提供了常用的机型配置:

这一步只是先让你们看,先放一边,我们通常安装的时候不会直接选择合适的驱动,而是先使用通用驱动来安装,他是以最小的通用兼容配置来做的,通常都能够进入安装并且顺利通过。

写入优盘

此时,你需要拥有一个 16G 的优盘!记住记住,一定是 16G 不然他镜像可以刻录但是不会帮你刻录完整,不完整的镜像就是一个无法安装的镜像。(我用了一个 8G 的在这一步卡了好几个小时,最后把公司的师弟杀了祭天了,他出现了警告也不看直接点击 OK 就给我刻录了一个废盘) 首先你需要一个软件:TransMac 当然因为我的习惯,所以我都是在 Windows 系统下写优盘的。 安装,然后右键 -> 以管理员身份运行

会有提示需要购买正版什么的,不过点击 run 可以直接运行试用版,试用版就够用了。 然后找到你的优盘,右键选择 Format Disk –> Format with Disk Image 即可。

漫长的等待,结束了就可以使用这个优盘来安装系统了。

修改BIOS引导

msruefi,只能选择后者。一般新买的电脑或者近几年的电脑都是这个。 简单的识别方法:开机的时候,如果是品牌 Logo 然后就开始转圈圈进入 Windows 系统的话,就是 uefi。 第二就是修改 biossecurity boot,直接关闭就完事了。如果同等级的还有说什么引导什么系统的话,记得把它修改成 other

如果需要双系统

如果不需要双系统安装,直接跳过这一段

双系统指的是 Windows + osx,好像一般初学者都需要这个东西(包括之前的我自己)? 首先首先,Windows 默认的引导分区太小了,如果你不重新安装 Windows 的话,直接安装 osx 后面都会导致任何软件都无法安装。 教程 类似于上面地址里面的教程,但是在创建 efi 的时候,大小给大点,我一般给的是 512m,因为后面我们可能还需要根据不同的机型,在这个分区里面放入驱动。 分好区,先把 Windows 装上,记得留一些硬盘给 osx,我建议是 100G

装好系统后,在硬盘管理那里给 efi 分区随便给个盘符,然后把里面的东西备份一份出来,因为待会需要对他开刀,备份完整不至于你总是需要重新安装系统。

优盘引导开机

为了安装方便我建议将 first boot 设置为上面的优盘。当然使用 F12 来选择也行不过中途需要重启的时候就要留意重新选择了。 选择优盘中的带有 UEFI 选项的,这时候电脑将会读取优盘的引导,进入引导界面。

进入优盘是这个样子的:

一般是在第一项,下面会提示 Boot macOS Install from xxx 后面一般是优盘它默认给的名字。此时,我们 需要按一次 空格键 ,来到下一个页面:

下面是启动选项,一般按下空格需要设定的,大佬已经帮我们写好了,这个时候直接 按下回车键 进入安装软件就可以了。 一串命令后,我们会进来到这个页面:

能够来到这里,说明安装过程十分顺利,接下来进入下一个环节。

硬盘分区

就跟上面一样,我们选择磁盘工具,然后点击 继续,就会进入到磁盘工具的页面,那我们先选择显示所有设备,这时候磁盘都会显示出来。

然后我们选择整个硬盘(如果是单系统的话,双系统的话,你需要选择你要格式化的那个分区)点击上面的抹掉(看我鼠标那个位置):

点击之后会出来这个硬盘设定界面:

  1. 名称,可以自定义;

  2. 格式,新出来的文件系统是 APFS 听说是提速以及增加安全性的,那我就用这个好了,也建议使用这个。强烈不要使用区分大小写的分区格式(敲黑板),因为我使用了大小写区分的硬盘导致有些软件它不支持!!我目前就是因为这个原因重新安装系统的;

  3. 方案选择 GUID 分区图

稍稍等待一番,成功以后,直接左上角关闭磁盘管理工具。我们就可以安装系统了。

安装系统

回到这个界面,选择 安装 macOS


然后就同意协议了,这个不用图都知道要选择什么吧… 同意协议之后就是选择硬盘了,在这个界面,没有意外的话应该可以看到我们刚刚分出来的硬盘分区:

选好硬盘点击安装,接下来进入漫长的等待:

!!!请注意!!!几分钟动一次鼠标,不要让电脑进入睡眠,因为可能因为驱动原因会睡死。 然后系统在安装过程需要一次重启,别担心,如果你在之前已经设置好优盘第一启动了的话,就不用管它,没有的话,需要手动选择优盘引导,进入优盘不用做什么,会继续安装:

跑完基本就可以进入系统的基础设置页面了:

选择什么不用说了吧,选择其他国家也可以,只要看得懂就好…

完成安装

完成安装可以进入桌面了,左上角有个 -> 关于本机

哎哟卧槽,这个显存不对劲啊….. 那现在我们暂时忍一下,使用憋屈的动画效果使用一下 Safari,去下载一个软件:Clover Configurator 安装软件

LaunchPad 打开运行,不出意外的话:

LaunchPad/其他/终端 运行以下命令,输入设置的用户密码就可以了:

1
sudo spctl --master-disable

重新打开 Clover Configurator 来到这个菜单:

我们现在的引导还是优盘,所以先在优盘那一项点击后面的挂载,这时候应该需要输入用户的密码,挂载成功完以后,就可以在 访达 看到 EFI 分区了。

还记得这张图吗:

将原来的

config.plist 备份一份,把这里面的适合自己机型的配置,改名为 config.plist 放在上一层文件夹:

然后重启电脑,进入优盘引导,根据下面字样提示,选择 Boot macOS from 你设置的分区名 即可进入安装好的系统。如果配置兼容的话,重新查看关于本机:

显存正常了。 再尝试音乐,如果声卡也正常,那就完成安装了。

迁入硬盘引导

一切好了,总不能总是插着硬盘来开机吧,我们需要把硬盘的文件替换到磁盘里面去,还是使用 Clover Configurator

一个是硬盘的,一个是优盘的,同时挂载出来。 然后使用 访达 进行操作:

OK,bios 重新设置第一启动是硬盘,然后,如果需要进入 osx 则选择 osx 硬盘回车,需要 Windows 的话就选择 Windows 所在的分区即可。

其他设置

这是我自己觉得比较好的设置,每个人可以按照自己的习惯来。 当然关于原来在 Windows 习惯的 Ctrl + c 这些都需要换成 Command + C 了,Command 在原来的 Win键 上面,我是不建议在系统设置里面替换这两者的按键位置的,毕竟用起来还蛮舒服的。 然后所有软件需要进入设置界面,都已经统一是 Command + , 这个快捷键了。

访达小设置

访达默认设置我感觉是不太好用的,比如点击访达,显示的不是我的电脑的内容,而是最近使用的内容,我特么写代码最近使用的内容都是代码文件,而我们通常不需要在这里去访问这些文件的(都是通过 IDE 工具),所以我希望点击访达进来的时候就是 我的个人文件夹: 打开 访达Command + ,

第二个不好的点是,我去到那个文件夹,要搜索某个文件,默认给我跑整个电脑去搜索,速度慢又没必要,所以:

第三个不好的点是,文件默认居然不排序,还要像垃圾堆一样堆在一起!!改掉设置!: 这个不在 Command + , 里边了,在这里:

开启Retina渲染

参考资料: 黑苹果开启缩放分辨率HiDPi以及字体模糊的调整方法总结 黑苹果开启HiDPI“解决”字体模糊的问题 直接在 其他软件/终端 粘贴下面的命令运行,提示输入用户密码,输入完以后根据提示来走就可以了:

1
sh -c "$(curl -fsSL https://raw.githubusercontent.com/xzhih/one-key-hidpi/master/hidpi.sh)"

重启电脑后安装 RDM 工具来配置分辨率(下载 RDM-2.2.dmg 就可以了 ) 运行,右上角:

选择带有小闪电的就是 HiDPI 的设置(也就是 Retina 效果)

一般根据屏幕原始分辨率来定,有 4K 显示器开 2K 分辨率的话,效果就跟白苹果一样了,2倍的差距,我目前原始分辨率是 1920 * 1080,开图中的分辨率也就凑合凑合过吧

然后在 系统偏好设置 –> 通用 里面开启字体平滑(在最下面,默认也是开启的)

我对默认的平滑程度还是不太喜欢,要开最高的,终端运行以下命令,提示输入用户密码,输入完以后重启即可:

1
defaults -currentHost write -globalDomain AppleFontSmoothing -int 3

来看看效果图: 开启前: 开启后:

结束

后面有空再更更常用软件了。

选择带有小闪电的就是 HiDPI 的设置(也就是 Retina 效果)

一般根据屏幕原始分辨率来定,有 4K 显示器开 2K 分辨率的话,效果就跟白苹果一样了,2倍的差距,我目前原始分辨率是 1920 * 1080,开图中的分辨率也就凑合凑合过吧

然后在 系统偏好设置 –> 通用 里面开启字体平滑(在最下面,默认也是开启的)

从大学就开始折腾黑苹果,迟迟没有做些记录,总觉得差点什么,所以这次趁我需要重装系统的机会,拍了些照片,结合一些文字来做一个教程吧。 阅读按照文章顺序来,不要跳着来,除非我注明让你可以跳过。

OSX系统

不做评价谁好谁差,但是作为 Web工程师 的话,在 osx 系统下的编程体验确实要比 Windows 下好很多,软件也特别干净,从来没有在你工作的时候,突然右下角来个弹窗给你看广告,或者直接把广告就弹在最顶层来影响你的工作。 jdk npm git Dart Flutter 这些程序,在 unix 系的 osx 下也从来没有报错。所以还是推荐使用的。

安装须知

台式机

在我看来台式机安装黑果是比较完美的,能够屏蔽独立显卡,没有 WiFi 的顾虑。而且较高的性能使得能够更加完美的体验 osx 系统。 但是还是有些限制你需要知道:

  1. Intel 芯支持比较完美,如果是 AMD 芯的话,虽然说现在有破解,但是我还是觉得不如自然免驱的好(无测试过,没什么发言权);
  2. 一般比较热门的独立显卡,在国内远景论坛都有驱动的教程,涉及一些代码的注入,我比较懒,所以公司的电脑就拆掉了独显,使用集显来驱动,集显也是免驱的;

笔记本

  1. 笔记本受限比较大,现在笔记本一般都是 集显+独显 来自动切换达到省电的功能的,但是白苹果的笔记本,不是一个独显就是一个集显。所以,两者混合的情况下一般只能驱动集显(除非部分笔记本可以屏蔽集显),显卡问题就是独显基本就废了;
  2. WiFi 常年无解,两个方法解决:
    1. 淘宝购买 usb 接口的免驱网卡,卖家通常会提供使用所需要的驱动和软件;
    2. 更换笔记本内部的网卡,这一步比较专业,加上联想笔记本更换配件是需要刷白名单的,所以通常来说,都会选择第一种方式解决。

安装流程

镜像下载

因为在这篇文章产生的时候最新的 osx 系统是 10.15 catalina。所以我就是用这个系统来做教程吧。 osx_10.15_catalina 下载地址:下载地址

感谢远景论坛大佬的封装,我没有直接放大佬的最终下载地址,因为我还是希望各位能够给去下载,大佬有硬币赚。

镜像已经下好了,大佬也提供了常用的机型配置:

这一步只是先让你们看,先放一边,我们通常安装的时候不会直接选择合适的驱动,而是先使用通用驱动来安装,他是以最小的通用兼容配置来做的,通常都能够进入安装并且顺利通过。

写入优盘

此时,你需要拥有一个 16G 的优盘!记住记住,一定是 16G 不然他镜像可以刻录但是不会帮你刻录完整,不完整的镜像就是一个无法安装的镜像。(我用了一个 8G 的在这一步卡了好几个小时,最后把公司的师弟杀了祭天了,他出现了警告也不看直接点击 OK 就给我刻录了一个废盘) 首先你需要一个软件:TransMac 当然因为我的习惯,所以我都是在 Windows 系统下写优盘的。 安装,然后右键 -> 以管理员身份运行

会有提示需要购买正版什么的,不过点击 run 可以直接运行试用版,试用版就够用了。 然后找到你的优盘,右键选择 Format Disk –> Format with Disk Image 即可。

漫长的等待,结束了就可以使用这个优盘来安装系统了。

修改BIOS引导

msruefi,只能选择后者。一般新买的电脑或者近几年的电脑都是这个。 简单的识别方法:开机的时候,如果是品牌 Logo 然后就开始转圈圈进入 Windows 系统的话,就是 uefi。 第二就是修改 biossecurity boot,直接关闭就完事了。如果同等级的还有说什么引导什么系统的话,记得把它修改成 other

如果需要双系统

如果不需要双系统安装,直接跳过这一段

双系统指的是 Windows + osx,好像一般初学者都需要这个东西(包括之前的我自己)? 首先首先,Windows 默认的引导分区太小了,如果你不重新安装 Windows 的话,直接安装 osx 后面都会导致任何软件都无法安装。 教程 类似于上面地址里面的教程,但是在创建 efi 的时候,大小给大点,我一般给的是 512m,因为后面我们可能还需要根据不同的机型,在这个分区里面放入驱动。 分好区,先把 Windows 装上,记得留一些硬盘给 osx,我建议是 100G

装好系统后,在硬盘管理那里给 efi 分区随便给个盘符,然后把里面的东西备份一份出来,因为待会需要对他开刀,备份完整不至于你总是需要重新安装系统。

优盘引导开机

为了安装方便我建议将 first boot 设置为上面的优盘。当然使用 F12 来选择也行不过中途需要重启的时候就要留意重新选择了。 选择优盘中的带有 UEFI 选项的,这时候电脑将会读取优盘的引导,进入引导界面。

进入优盘是这个样子的:

一般是在第一项,下面会提示 Boot macOS Install from xxx 后面一般是优盘它默认给的名字。此时,我们 需要按一次 空格键 ,来到下一个页面:

下面是启动选项,一般按下空格需要设定的,大佬已经帮我们写好了,这个时候直接 按下回车键 进入安装软件就可以了。 一串命令后,我们会进来到这个页面:

能够来到这里,说明安装过程十分顺利,接下来进入下一个环节。

硬盘分区

就跟上面一样,我们选择磁盘工具,然后点击 继续,就会进入到磁盘工具的页面,那我们先选择显示所有设备,这时候磁盘都会显示出来。

然后我们选择整个硬盘(如果是单系统的话,双系统的话,你需要选择你要格式化的那个分区)点击上面的抹掉(看我鼠标那个位置):

点击之后会出来这个硬盘设定界面:

  1. 名称,可以自定义;

  2. 格式,新出来的文件系统是 APFS 听说是提速以及增加安全性的,那我就用这个好了,也建议使用这个。强烈不要使用区分大小写的分区格式(敲黑板),因为我使用了大小写区分的硬盘导致有些软件它不支持!!我目前就是因为这个原因重新安装系统的;

  3. 方案选择 GUID 分区图

稍稍等待一番,成功以后,直接左上角关闭磁盘管理工具。我们就可以安装系统了。

安装系统

回到这个界面,选择 安装 macOS


然后就同意协议了,这个不用图都知道要选择什么吧… 同意协议之后就是选择硬盘了,在这个界面,没有意外的话应该可以看到我们刚刚分出来的硬盘分区:

选好硬盘点击安装,接下来进入漫长的等待:

!!!请注意!!!几分钟动一次鼠标,不要让电脑进入睡眠,因为可能因为驱动原因会睡死。 然后系统在安装过程需要一次重启,别担心,如果你在之前已经设置好优盘第一启动了的话,就不用管它,没有的话,需要手动选择优盘引导,进入优盘不用做什么,会继续安装:

跑完基本就可以进入系统的基础设置页面了:

选择什么不用说了吧,选择其他国家也可以,只要看得懂就好…

完成安装

完成安装可以进入桌面了,左上角有个 -> 关于本机

哎哟卧槽,这个显存不对劲啊….. 那现在我们暂时忍一下,使用憋屈的动画效果使用一下 Safari,去下载一个软件:Clover Configurator 安装软件

LaunchPad 打开运行,不出意外的话:

LaunchPad/其他/终端 运行以下命令,输入设置的用户密码就可以了:

1
sudo spctl --master-disable

重新打开 Clover Configurator 来到这个菜单:

我们现在的引导还是优盘,所以先在优盘那一项点击后面的挂载,这时候应该需要输入用户的密码,挂载成功完以后,就可以在 访达 看到 EFI 分区了。

还记得这张图吗:

将原来的

config.plist 备份一份,把这里面的适合自己机型的配置,改名为 config.plist 放在上一层文件夹:

然后重启电脑,进入优盘引导,根据下面字样提示,选择 Boot macOS from 你设置的分区名 即可进入安装好的系统。如果配置兼容的话,重新查看关于本机:

显存正常了。 再尝试音乐,如果声卡也正常,那就完成安装了。

迁入硬盘引导

一切好了,总不能总是插着硬盘来开机吧,我们需要把硬盘的文件替换到磁盘里面去,还是使用 Clover Configurator

一个是硬盘的,一个是优盘的,同时挂载出来。 然后使用 访达 进行操作:

OK,bios 重新设置第一启动是硬盘,然后,如果需要进入 osx 则选择 osx 硬盘回车,需要 Windows 的话就选择 Windows 所在的分区即可。

其他设置

这是我自己觉得比较好的设置,每个人可以按照自己的习惯来。 当然关于原来在 Windows 习惯的 Ctrl + c 这些都需要换成 Command + C 了,Command 在原来的 Win键 上面,我是不建议在系统设置里面替换这两者的按键位置的,毕竟用起来还蛮舒服的。 然后所有软件需要进入设置界面,都已经统一是 Command + , 这个快捷键了。

访达小设置

访达默认设置我感觉是不太好用的,比如点击访达,显示的不是我的电脑的内容,而是最近使用的内容,我特么写代码最近使用的内容都是代码文件,而我们通常不需要在这里去访问这些文件的(都是通过 IDE 工具),所以我希望点击访达进来的时候就是 我的个人文件夹: 打开 访达Command + ,

第二个不好的点是,我去到那个文件夹,要搜索某个文件,默认给我跑整个电脑去搜索,速度慢又没必要,所以:

第三个不好的点是,文件默认居然不排序,还要像垃圾堆一样堆在一起!!改掉设置!: 这个不在 Command + , 里边了,在这里:

开启Retina渲染

参考资料: 黑苹果开启缩放分辨率HiDPi以及字体模糊的调整方法总结 黑苹果开启HiDPI“解决”字体模糊的问题 直接在 其他软件/终端 粘贴下面的命令运行,提示输入用户密码,输入完以后根据提示来走就可以了:

1
sh -c "$(curl -fsSL https://raw.githubusercontent.com/xzhih/one-key-hidpi/master/hidpi.sh)"

重启电脑后安装 RDM 工具来配置分辨率(下载 RDM-2.2.dmg 就可以了 ) 运行,右上角:

选择带有小闪电的就是 HiDPI 的设置(也就是 Retina 效果)

一般根据屏幕原始分辨率来定,有 4K 显示器开 2K 分辨率的话,效果就跟白苹果一样了,2倍的差距,我目前原始分辨率是 1920 * 1080,开图中的分辨率也就凑合凑合过吧

然后在 系统偏好设置 –> 通用 里面开启字体平滑(在最下面,默认也是开启的)

我对默认的平滑程度还是不太喜欢,要开最高的,终端运行以下命令,提示输入用户密码,输入完以后重启即可:

1
defaults -currentHost write -globalDomain AppleFontSmoothing -int 3

来看看效果图: 开启前: 开启后:

结束

后面有空再更更常用软件了。

我对默认的平滑程度还是不太喜欢,要开最高的,终端运行以下命令,提示输入用户密码,输入完以后重启即可:

1
defaults -currentHost write -globalDomain AppleFontSmoothing -int 3

来看看效果图: 开启前:

从大学就开始折腾黑苹果,迟迟没有做些记录,总觉得差点什么,所以这次趁我需要重装系统的机会,拍了些照片,结合一些文字来做一个教程吧。 阅读按照文章顺序来,不要跳着来,除非我注明让你可以跳过。

OSX系统

不做评价谁好谁差,但是作为 Web工程师 的话,在 osx 系统下的编程体验确实要比 Windows 下好很多,软件也特别干净,从来没有在你工作的时候,突然右下角来个弹窗给你看广告,或者直接把广告就弹在最顶层来影响你的工作。 jdk npm git Dart Flutter 这些程序,在 unix 系的 osx 下也从来没有报错。所以还是推荐使用的。

安装须知

台式机

在我看来台式机安装黑果是比较完美的,能够屏蔽独立显卡,没有 WiFi 的顾虑。而且较高的性能使得能够更加完美的体验 osx 系统。 但是还是有些限制你需要知道:

  1. Intel 芯支持比较完美,如果是 AMD 芯的话,虽然说现在有破解,但是我还是觉得不如自然免驱的好(无测试过,没什么发言权);
  2. 一般比较热门的独立显卡,在国内远景论坛都有驱动的教程,涉及一些代码的注入,我比较懒,所以公司的电脑就拆掉了独显,使用集显来驱动,集显也是免驱的;

笔记本

  1. 笔记本受限比较大,现在笔记本一般都是 集显+独显 来自动切换达到省电的功能的,但是白苹果的笔记本,不是一个独显就是一个集显。所以,两者混合的情况下一般只能驱动集显(除非部分笔记本可以屏蔽集显),显卡问题就是独显基本就废了;
  2. WiFi 常年无解,两个方法解决:
    1. 淘宝购买 usb 接口的免驱网卡,卖家通常会提供使用所需要的驱动和软件;
    2. 更换笔记本内部的网卡,这一步比较专业,加上联想笔记本更换配件是需要刷白名单的,所以通常来说,都会选择第一种方式解决。

安装流程

镜像下载

因为在这篇文章产生的时候最新的 osx 系统是 10.15 catalina。所以我就是用这个系统来做教程吧。 osx_10.15_catalina 下载地址:下载地址

感谢远景论坛大佬的封装,我没有直接放大佬的最终下载地址,因为我还是希望各位能够给去下载,大佬有硬币赚。

镜像已经下好了,大佬也提供了常用的机型配置:

这一步只是先让你们看,先放一边,我们通常安装的时候不会直接选择合适的驱动,而是先使用通用驱动来安装,他是以最小的通用兼容配置来做的,通常都能够进入安装并且顺利通过。

写入优盘

此时,你需要拥有一个 16G 的优盘!记住记住,一定是 16G 不然他镜像可以刻录但是不会帮你刻录完整,不完整的镜像就是一个无法安装的镜像。(我用了一个 8G 的在这一步卡了好几个小时,最后把公司的师弟杀了祭天了,他出现了警告也不看直接点击 OK 就给我刻录了一个废盘) 首先你需要一个软件:TransMac 当然因为我的习惯,所以我都是在 Windows 系统下写优盘的。 安装,然后右键 -> 以管理员身份运行

会有提示需要购买正版什么的,不过点击 run 可以直接运行试用版,试用版就够用了。 然后找到你的优盘,右键选择 Format Disk –> Format with Disk Image 即可。

漫长的等待,结束了就可以使用这个优盘来安装系统了。

修改BIOS引导

msruefi,只能选择后者。一般新买的电脑或者近几年的电脑都是这个。 简单的识别方法:开机的时候,如果是品牌 Logo 然后就开始转圈圈进入 Windows 系统的话,就是 uefi。 第二就是修改 biossecurity boot,直接关闭就完事了。如果同等级的还有说什么引导什么系统的话,记得把它修改成 other

如果需要双系统

如果不需要双系统安装,直接跳过这一段

双系统指的是 Windows + osx,好像一般初学者都需要这个东西(包括之前的我自己)? 首先首先,Windows 默认的引导分区太小了,如果你不重新安装 Windows 的话,直接安装 osx 后面都会导致任何软件都无法安装。 教程 类似于上面地址里面的教程,但是在创建 efi 的时候,大小给大点,我一般给的是 512m,因为后面我们可能还需要根据不同的机型,在这个分区里面放入驱动。 分好区,先把 Windows 装上,记得留一些硬盘给 osx,我建议是 100G

装好系统后,在硬盘管理那里给 efi 分区随便给个盘符,然后把里面的东西备份一份出来,因为待会需要对他开刀,备份完整不至于你总是需要重新安装系统。

优盘引导开机

为了安装方便我建议将 first boot 设置为上面的优盘。当然使用 F12 来选择也行不过中途需要重启的时候就要留意重新选择了。 选择优盘中的带有 UEFI 选项的,这时候电脑将会读取优盘的引导,进入引导界面。

进入优盘是这个样子的:

一般是在第一项,下面会提示 Boot macOS Install from xxx 后面一般是优盘它默认给的名字。此时,我们 需要按一次 空格键 ,来到下一个页面:

下面是启动选项,一般按下空格需要设定的,大佬已经帮我们写好了,这个时候直接 按下回车键 进入安装软件就可以了。 一串命令后,我们会进来到这个页面:

能够来到这里,说明安装过程十分顺利,接下来进入下一个环节。

硬盘分区

就跟上面一样,我们选择磁盘工具,然后点击 继续,就会进入到磁盘工具的页面,那我们先选择显示所有设备,这时候磁盘都会显示出来。

然后我们选择整个硬盘(如果是单系统的话,双系统的话,你需要选择你要格式化的那个分区)点击上面的抹掉(看我鼠标那个位置):

点击之后会出来这个硬盘设定界面:

  1. 名称,可以自定义;

  2. 格式,新出来的文件系统是 APFS 听说是提速以及增加安全性的,那我就用这个好了,也建议使用这个。强烈不要使用区分大小写的分区格式(敲黑板),因为我使用了大小写区分的硬盘导致有些软件它不支持!!我目前就是因为这个原因重新安装系统的;

  3. 方案选择 GUID 分区图

稍稍等待一番,成功以后,直接左上角关闭磁盘管理工具。我们就可以安装系统了。

安装系统

回到这个界面,选择 安装 macOS


然后就同意协议了,这个不用图都知道要选择什么吧… 同意协议之后就是选择硬盘了,在这个界面,没有意外的话应该可以看到我们刚刚分出来的硬盘分区:

选好硬盘点击安装,接下来进入漫长的等待:

!!!请注意!!!几分钟动一次鼠标,不要让电脑进入睡眠,因为可能因为驱动原因会睡死。 然后系统在安装过程需要一次重启,别担心,如果你在之前已经设置好优盘第一启动了的话,就不用管它,没有的话,需要手动选择优盘引导,进入优盘不用做什么,会继续安装:

跑完基本就可以进入系统的基础设置页面了:

选择什么不用说了吧,选择其他国家也可以,只要看得懂就好…

完成安装

完成安装可以进入桌面了,左上角有个 -> 关于本机

哎哟卧槽,这个显存不对劲啊….. 那现在我们暂时忍一下,使用憋屈的动画效果使用一下 Safari,去下载一个软件:Clover Configurator 安装软件

LaunchPad 打开运行,不出意外的话:

LaunchPad/其他/终端 运行以下命令,输入设置的用户密码就可以了:

1
sudo spctl --master-disable

重新打开 Clover Configurator 来到这个菜单:

我们现在的引导还是优盘,所以先在优盘那一项点击后面的挂载,这时候应该需要输入用户的密码,挂载成功完以后,就可以在 访达 看到 EFI 分区了。

还记得这张图吗:

将原来的

config.plist 备份一份,把这里面的适合自己机型的配置,改名为 config.plist 放在上一层文件夹:

然后重启电脑,进入优盘引导,根据下面字样提示,选择 Boot macOS from 你设置的分区名 即可进入安装好的系统。如果配置兼容的话,重新查看关于本机:

显存正常了。 再尝试音乐,如果声卡也正常,那就完成安装了。

迁入硬盘引导

一切好了,总不能总是插着硬盘来开机吧,我们需要把硬盘的文件替换到磁盘里面去,还是使用 Clover Configurator

一个是硬盘的,一个是优盘的,同时挂载出来。 然后使用 访达 进行操作:

OK,bios 重新设置第一启动是硬盘,然后,如果需要进入 osx 则选择 osx 硬盘回车,需要 Windows 的话就选择 Windows 所在的分区即可。

其他设置

这是我自己觉得比较好的设置,每个人可以按照自己的习惯来。 当然关于原来在 Windows 习惯的 Ctrl + c 这些都需要换成 Command + C 了,Command 在原来的 Win键 上面,我是不建议在系统设置里面替换这两者的按键位置的,毕竟用起来还蛮舒服的。 然后所有软件需要进入设置界面,都已经统一是 Command + , 这个快捷键了。

访达小设置

访达默认设置我感觉是不太好用的,比如点击访达,显示的不是我的电脑的内容,而是最近使用的内容,我特么写代码最近使用的内容都是代码文件,而我们通常不需要在这里去访问这些文件的(都是通过 IDE 工具),所以我希望点击访达进来的时候就是 我的个人文件夹: 打开 访达Command + ,

第二个不好的点是,我去到那个文件夹,要搜索某个文件,默认给我跑整个电脑去搜索,速度慢又没必要,所以:

第三个不好的点是,文件默认居然不排序,还要像垃圾堆一样堆在一起!!改掉设置!: 这个不在 Command + , 里边了,在这里:

开启Retina渲染

参考资料: 黑苹果开启缩放分辨率HiDPi以及字体模糊的调整方法总结 黑苹果开启HiDPI“解决”字体模糊的问题 直接在 其他软件/终端 粘贴下面的命令运行,提示输入用户密码,输入完以后根据提示来走就可以了:

1
sh -c "$(curl -fsSL https://raw.githubusercontent.com/xzhih/one-key-hidpi/master/hidpi.sh)"

重启电脑后安装 RDM 工具来配置分辨率(下载 RDM-2.2.dmg 就可以了 ) 运行,右上角:

选择带有小闪电的就是 HiDPI 的设置(也就是 Retina 效果)

一般根据屏幕原始分辨率来定,有 4K 显示器开 2K 分辨率的话,效果就跟白苹果一样了,2倍的差距,我目前原始分辨率是 1920 * 1080,开图中的分辨率也就凑合凑合过吧

然后在 系统偏好设置 –> 通用 里面开启字体平滑(在最下面,默认也是开启的)

我对默认的平滑程度还是不太喜欢,要开最高的,终端运行以下命令,提示输入用户密码,输入完以后重启即可:

1
defaults -currentHost write -globalDomain AppleFontSmoothing -int 3

来看看效果图: 开启前: 开启后:

结束

后面有空再更更常用软件了。

结束

后面有空再更更常用软件了。